initial commit

This commit is contained in:
Gitea
2020-11-13 14:13:12 -05:00
commit 05df49ff60
368 changed files with 128754 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,441 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local BuyUtil = TSM.UI.AuctionUI:NewPackage("BuyUtil")
local L = TSM.Include("Locale").GetTable()
local Money = TSM.Include("Util.Money")
local Log = TSM.Include("Util.Log")
local Math = TSM.Include("Util.Math")
local Theme = TSM.Include("Util.Theme")
local ItemInfo = TSM.Include("Service.ItemInfo")
local CustomPrice = TSM.Include("Service.CustomPrice")
local UIElements = TSM.Include("UI.UIElements")
local private = {
totalBuyout = nil,
isBuy = nil,
auctionScan = nil,
subRow = nil,
index = nil,
noSeller = nil,
baseFrame = nil,
dialogFrame = nil,
future = nil,
prepareQuantity = nil,
prepareSuccess = false,
marketValueFunc = nil,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function BuyUtil.ShowConfirmation(baseFrame, subRow, isBuy, auctionNum, numFound, maxQuantity, callback, auctionScan, index, noSeller, marketValueFunc)
auctionNum = min(auctionNum, numFound)
local buyout = subRow:GetBuyouts()
if not isBuy then
buyout = subRow:GetRequiredBid(subRow)
end
local quantity = subRow:GetQuantities()
local itemString = subRow:GetItemString()
local _, _, _, isHighBidder = subRow:GetBidInfo()
local isCommodity = not TSM.IsWowClassic() and subRow:IsCommodity()
local shouldConfirm = false
if isCommodity then
shouldConfirm = true
elseif isBuy and isHighBidder then
shouldConfirm = true
elseif TSM.db.global.shoppingOptions.buyoutConfirm then
shouldConfirm = ceil(buyout / quantity) >= (CustomPrice.GetValue(TSM.db.global.shoppingOptions.buyoutAlertSource, itemString) or 0)
end
if not shouldConfirm then
return false
end
baseFrame = baseFrame:GetBaseElement()
private.isBuy = isBuy
private.auctionScan = auctionScan
private.subRow = subRow
private.index = index
private.noSeller = noSeller
private.baseFrame = baseFrame
private.marketValueFunc = marketValueFunc
if private.dialogFrame then
return true
end
local defaultQuantity = isCommodity and numFound or 1
assert(not isCommodity or isBuy)
local displayItemBuyout, displayTotalBuyout = nil, nil
if isCommodity then
displayTotalBuyout = private.CommodityResultsByQuantity(itemString, defaultQuantity)
displayItemBuyout = Math.Ceil(displayTotalBuyout / defaultQuantity, COPPER_PER_SILVER)
else
displayItemBuyout = ceil(buyout / quantity)
displayTotalBuyout = TSM.IsWowClassic() and buyout or ceil(buyout / quantity)
end
private.dialogFrame = UIElements.New("Frame", "frame")
:SetLayout("VERTICAL")
:SetSize(isCommodity and 600 or 326, isCommodity and 272 or 262)
:SetPadding(12)
:AddAnchor("CENTER")
:SetContext(callback)
:SetMouseEnabled(true)
:SetBackgroundColor("FRAME_BG", true)
:AddChild(UIElements.New("Frame", "header")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(0, 0, -4, 10)
:AddChild(UIElements.New("Spacer", "spacer")
:SetWidth(20)
)
:AddChild(UIElements.New("Text", "title")
:SetJustifyH("CENTER")
:SetFont("BODY_BODY1_BOLD")
:SetText(isCommodity and L["Order Confirmation"] or L["Buyout"])
)
:AddChild(UIElements.New("Button", "closeBtn")
:SetMargin(0, -4, 0, 0)
:SetBackgroundAndSize("iconPack.24x24/Close/Default")
:SetScript("OnClick", private.BuyoutConfirmCloseBtnOnClick)
)
)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("HORIZONTAL")
:AddChild(UIElements.New("Frame", "left")
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Frame", "item")
:SetLayout("HORIZONTAL")
:SetPadding(6)
:SetMargin(0, 0, 0, 16)
:SetBackgroundColor("PRIMARY_BG_ALT", true)
:AddChild(UIElements.New("Button", "icon")
:SetSize(36, 36)
:SetMargin(0, 8, 0, 0)
:SetBackground(ItemInfo.GetTexture(itemString))
:SetTooltip(itemString)
)
:AddChild(UIElements.New("Text", "name")
:SetHeight(36)
:SetFont("ITEM_BODY1")
:SetText(TSM.UI.GetColoredItemName(itemString))
)
)
:AddChildIf(isCommodity, UIElements.New("Frame", "quantity")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Text", "desc")
:SetFont("BODY_BODY2")
:SetText(L["Quantity"]..":")
)
:AddChild(UIElements.New("Input", "input")
:SetWidth(140)
:SetJustifyH("RIGHT")
:SetBackgroundColor("PRIMARY_BG_ALT")
:SetValidateFunc("NUMBER", "1:"..maxQuantity)
:SetValue(tostring(defaultQuantity))
:SetContext(maxQuantity)
:SetScript("OnValueChanged", private.InputQtyOnValueChanged)
)
)
:AddChildIf(not isCommodity, UIElements.New("Frame", "stacks")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 0, 10)
:AddChild(UIElements.New("Text", "desc")
:SetWidth("AUTO")
:SetFont("BODY_BODY2")
:SetText(isBuy and L["Purchasing Auction"]..":" or L["Bidding Auction"]..":")
)
:AddChild(UIElements.New("Text", "number")
:SetJustifyH("RIGHT")
:SetFont("TABLE_TABLE1")
:SetText(auctionNum.."/"..numFound)
)
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Frame", "price")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 0, 10)
:AddChild(UIElements.New("Text", "desc")
:SetWidth("AUTO")
:SetFont("BODY_BODY2")
:SetText(L["Unit Price"]..":")
)
:AddChild(UIElements.New("Text", "money")
:SetJustifyH("RIGHT")
:SetFont("TABLE_TABLE1")
:SetContext(displayItemBuyout)
:SetText(private.GetUnitPriceMoneyStr(displayItemBuyout))
)
)
:AddChild(UIElements.New("Frame", "total")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 0, 10)
:AddChild(UIElements.New("Text", "desc")
:SetWidth("AUTO")
:SetFont("BODY_BODY2")
:SetText(L["Total Price"]..":")
)
:AddChild(UIElements.New("Text", "money")
:SetJustifyH("RIGHT")
:SetFont("TABLE_TABLE1")
:SetText(Money.ToString(displayTotalBuyout, nil, "OPT_83_NO_COPPER"))
)
)
:AddChild(UIElements.New("Text", "warning")
:SetHeight(20)
:SetMargin(0, 0, 0, 10)
:SetFont("BODY_BODY3")
:SetJustifyH("CENTER")
:SetTextColor(Theme.GetFeedbackColor("YELLOW"))
:SetText("")
)
:AddChild(UIElements.NewNamed("ActionButton", "confirmBtn", "TSMBidBuyConfirmBtn")
:SetHeight(24)
:SetText(isCommodity and L["Buy Commodity"] or (isBuy and L["Buy Auction"] or L["Bid Auction"]))
:SetContext(not isCommodity and quantity or nil)
:SetDisabled(private.future and true or false)
:SetScript("OnClick", private.ConfirmBtnOnClick)
)
)
:AddChildIf(isCommodity, UIElements.New("Frame", "item")
:SetLayout("HORIZONTAL")
:SetWidth(266)
:SetMargin(12, 0, 0, 0)
:SetPadding(4, 4, 8, 8)
:SetBackgroundColor("PRIMARY_BG_ALT", true)
:AddChild(UIElements.New("CommodityList", "items")
:SetBackgroundColor("PRIMARY_BG_ALT")
:SetData(subRow:GetResultRow())
:SetMarketValueFunction(marketValueFunc)
:SetAlertThreshold(TSM.db.global.shoppingOptions.buyoutConfirm and (CustomPrice.GetValue(TSM.db.global.shoppingOptions.buyoutAlertSource, itemString) or 0) or nil)
:SelectQuantity(defaultQuantity)
:SetScript("OnRowClick", private.CommodityOnRowClick)
)
)
)
:SetScript("OnHide", private.DialogOnHide)
baseFrame:ShowDialogFrame(private.dialogFrame)
private.prepareQuantity = nil
private.Prepare(defaultQuantity)
return true
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.DialogOnHide()
if private.future then
private.future:Cancel()
private.future = nil
end
private.baseFrame = nil
private.dialogFrame = nil
private.isBuy = nil
private.auctionScan = nil
private.subRow = nil
private.index = nil
private.noSeller = nil
end
function private.BuyoutConfirmCloseBtnOnClick(button)
button:GetBaseElement():HideDialog()
end
function private.EnableFutureOnDone()
local result = private.future:GetValue()
private.future = nil
if not result or result ~= (private.subRow:IsCommodity() and private.totalBuyout or select(2, private.subRow:GetBuyouts())) then
-- the unit price changed
Log.PrintUser(L["Failed to buy auction."])
private.baseFrame:HideDialog()
return
end
local input = private.dialogFrame:GetElement("content.left.quantity.input")
private.prepareSuccess = tonumber(input:GetValue()) == private.prepareQuantity
private.UpdateConfirmButton()
end
function private.GetUnitPriceMoneyStr(itemBuyout)
local priceStr = Money.ToString(itemBuyout, nil, "OPT_83_NO_COPPER")
local marketValueStr = nil
local marketValue = private.marketValueFunc(private.subRow)
local pct = marketValue and marketValue > 0 and itemBuyout > 0 and Math.Round(100 * itemBuyout / marketValue) or nil
if pct then
local pctColor = Theme.GetAuctionPercentColor(pct)
if pct > 999 then
marketValueStr = pctColor:ColorText(">999%")
else
marketValueStr = pctColor:ColorText(pct.."%")
end
else
marketValueStr = "---"
end
return format("%s (%s)", priceStr, marketValueStr)
end
function private.CommodityResultsByQuantity(itemString, quantity)
local remainingQuantity = quantity
local totalPrice, maxPrice = 0, 0
for _, query in private.auctionScan:QueryIterator() do
for _, subRow in query:ItemSubRowIterator(itemString) do
if remainingQuantity > 0 then
local _, itemBuyout = subRow:GetBuyouts()
local _, numOwnerItems = subRow:GetOwnerInfo()
local quantityAvailable = subRow:GetQuantities() - numOwnerItems
local quantityToBuy = min(quantityAvailable, remainingQuantity)
totalPrice = totalPrice + (itemBuyout * quantityToBuy)
remainingQuantity = remainingQuantity - quantityToBuy
maxPrice = max(maxPrice, itemBuyout)
end
end
end
return totalPrice, maxPrice
end
function private.InputQtyOnValueChanged(input, noListUpdate)
local quantity = tonumber(input:GetValue())
input:SetValue(quantity)
local totalBuyout = private.subRow:IsCommodity() and private.CommodityResultsByQuantity(private.subRow:GetItemString(), quantity) or input:GetElement("__parent.__parent.price.money"):GetContext() * quantity
local totalQuantity = quantity
local itemBuyout = totalQuantity > 0 and Math.Ceil(totalBuyout / totalQuantity, COPPER_PER_SILVER) or 0
input:GetElement("__parent.__parent.price.money")
:SetContext(itemBuyout)
:SetText(private.GetUnitPriceMoneyStr(itemBuyout))
:Draw()
input:GetElement("__parent.__parent.total.money")
:SetText(Money.ToString(totalBuyout, nil, "OPT_83_NO_COPPER"))
:Draw()
if not noListUpdate then
input:GetElement("__parent.__parent.__parent.item.items")
:SelectQuantity(quantity)
end
if quantity ~= private.prepareQuantity then
private.prepareSuccess = false
private.prepareQuantity = nil
end
private.UpdateConfirmButton()
end
function private.CommodityOnRowClick(list, index)
local input = list:GetElement("__parent.__parent.left.quantity.input")
input:SetValue(list:GetTotalQuantity(index))
:Draw()
private.Prepare(tonumber(input:GetValue()))
private.InputQtyOnValueChanged(input, true)
end
function private.ConfirmBtnOnClick(button)
local inputQuantity = nil
if not TSM.IsWowClassic() and private.subRow:IsCommodity() then
local input = private.dialogFrame:GetElement("content.left.quantity.input")
inputQuantity = tonumber(input:GetValue())
if not private.prepareSuccess and not TSM.IsWowClassic() then
-- this is a prepare click
private.Prepare(inputQuantity)
return
end
assert(private.prepareQuantity == inputQuantity)
end
local isBuy = private.isBuy
local callbackQuantity = button:GetContext()
if callbackQuantity == nil then
assert(inputQuantity)
callbackQuantity = inputQuantity
end
local callback = button:GetElement("__parent.__parent.__parent"):GetContext()
button:GetBaseElement():HideDialog()
callback(isBuy, callbackQuantity)
end
function private.UpdateConfirmButton()
local confirmBtn = private.dialogFrame:GetElement("content.left.confirmBtn")
local text, disabled, requireManualClick = nil, false, false
if not TSM.IsWowClassic() and private.subRow:IsCommodity() then
local input = confirmBtn:GetElement("__parent.quantity.input")
local inputQuantity = tonumber(input:GetValue())
local minQuantity = 1
local maxQuantity = confirmBtn:GetElement("__parent.quantity.input"):GetContext()
local itemString = private.subRow:GetItemString()
local totalCost, maxCost = private.CommodityResultsByQuantity(itemString, inputQuantity)
local alertThreshold = TSM.db.global.shoppingOptions.buyoutConfirm and (CustomPrice.GetValue(TSM.db.global.shoppingOptions.buyoutAlertSource, itemString) or 0) or math.huge
if maxCost >= alertThreshold then
requireManualClick = true
confirmBtn:GetElement("__parent.warning")
:SetText(L["Contains auctions above your alert threshold!"])
:Draw()
else
confirmBtn:GetElement("__parent.warning")
:SetText("")
:Draw()
end
if GetMoney() < totalCost then
text = L["Not Enough Money"]
disabled = true
elseif totalCost <= 0 or inputQuantity < minQuantity or inputQuantity > maxQuantity then
text = L["Invalid Quantity"]
disabled = true
elseif private.prepareSuccess or TSM.IsWowClassic() then
text = L["Buy Commodity"]
disabled = false
elseif private.prepareQuantity then
text = L["Preparing..."]
disabled = true
else
text = private.isBuy and L["Prepare Buy"] or L["Prepare Bid"]
disabled = false
end
else
if GetMoney() < confirmBtn:GetElement("__parent.price.money"):GetContext() then
text = L["Not Enough Money"]
disabled = true
else
text = private.isBuy and L["Buy Auction"] or L["Bid Auction"]
disabled = false
end
end
confirmBtn:SetText(text)
:SetDisabled(disabled)
:SetRequireManualClick(requireManualClick)
:Draw()
end
function private.Prepare(quantity)
if quantity == private.prepareQuantity then
return
end
if private.future then
private.future:Cancel()
private.future = nil
end
private.prepareQuantity = quantity
private.prepareSuccess = false
local totalBuyout = not TSM.IsWowClassic() and private.subRow:IsCommodity() and private.CommodityResultsByQuantity(private.subRow:GetItemString(), quantity) or (select(2, private.subRow:GetBuyouts()))
local totalQuantity = quantity
private.totalBuyout = totalBuyout
local itemBuyout = totalQuantity > 0 and Math.Ceil(totalBuyout / totalQuantity, COPPER_PER_SILVER)
local result, future = private.auctionScan:PrepareForBidOrBuyout(private.index, private.subRow, private.noSeller, quantity, itemBuyout)
if not result then
private.prepareQuantity = nil
return
elseif future then
private.future = future
future:SetScript("OnDone", private.EnableFutureOnDone)
end
private.UpdateConfirmButton()
end

271
Core/UI/AuctionUI/Core.lua Normal file
View File

@@ -0,0 +1,271 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local AuctionUI = TSM.UI:NewPackage("AuctionUI")
local L = TSM.Include("Locale").GetTable()
local Delay = TSM.Include("Util.Delay")
local Event = TSM.Include("Util.Event")
local Log = TSM.Include("Util.Log")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Settings = TSM.Include("Service.Settings")
local ItemLinked = TSM.Include("Service.ItemLinked")
local UIElements = TSM.Include("UI.UIElements")
local private = {
settings = nil,
topLevelPages = {},
frame = nil,
hasShown = false,
isSwitching = false,
scanningPage = nil,
updateCallbacks = {},
defaultFrame = nil,
}
local MIN_FRAME_SIZE = { width = 750, height = 450 }
-- ============================================================================
-- Module Functions
-- ============================================================================
function AuctionUI.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "auctionUIContext", "showDefault")
:AddKey("global", "auctionUIContext", "frame")
UIParent:UnregisterEvent("AUCTION_HOUSE_SHOW")
Event.Register("AUCTION_HOUSE_SHOW", private.AuctionFrameInit)
Event.Register("AUCTION_HOUSE_CLOSED", private.HideAuctionFrame)
if TSM.IsWowClassic() then
Delay.AfterTime(1, function() LoadAddOn("Blizzard_AuctionUI") end)
else
Delay.AfterTime(1, function() LoadAddOn("Blizzard_AuctionHouseUI") end)
end
ItemLinked.RegisterCallback(private.ItemLinkedCallback)
end
function AuctionUI.OnDisable()
if private.frame then
-- hide the frame
private.frame:Hide()
assert(not private.frame)
end
end
function AuctionUI.RegisterTopLevelPage(name, callback, itemLinkedHandler)
tinsert(private.topLevelPages, { name = name, callback = callback, itemLinkedHandler = itemLinkedHandler })
end
function AuctionUI.StartingScan(pageName)
if private.scanningPage and private.scanningPage ~= pageName then
Log.PrintfUser(L["A scan is already in progress. Please stop that scan before starting another one."])
return false
end
private.scanningPage = pageName
Log.Info("Starting scan %s", pageName)
if private.frame then
private.frame:SetPulsingNavButton(private.scanningPage)
end
for _, callback in ipairs(private.updateCallbacks) do
callback()
end
return true
end
function AuctionUI.EndedScan(pageName)
if private.scanningPage == pageName then
Log.Info("Ended scan %s", pageName)
private.scanningPage = nil
if private.frame then
private.frame:SetPulsingNavButton()
end
for _, callback in ipairs(private.updateCallbacks) do
callback()
end
end
end
function AuctionUI.SetOpenPage(name)
private.frame:SetSelectedNavButton(name, true)
end
function AuctionUI.IsPageOpen(name)
if not private.frame then
return false
end
return private.frame:GetSelectedNavButton() == name
end
function AuctionUI.IsScanning()
return private.scanningPage and true or false
end
function AuctionUI.RegisterUpdateCallback(callback)
tinsert(private.updateCallbacks, callback)
end
function AuctionUI.IsVisible()
return private.frame and true or false
end
-- ============================================================================
-- Main Frame
-- ============================================================================
function private.AuctionFrameInit()
local tabTemplateName = nil
if TSM.IsWowClassic() then
private.defaultFrame = AuctionFrame
tabTemplateName = "AuctionTabTemplate"
else
private.defaultFrame = AuctionHouseFrame
tabTemplateName = "AuctionHouseFrameTabTemplate"
end
if not private.hasShown then
private.hasShown = true
local tabId = private.defaultFrame.numTabs + 1
local tab = CreateFrame("Button", "AuctionFrameTab"..tabId, private.defaultFrame, tabTemplateName)
tab:Hide()
tab:SetID(tabId)
tab:SetText(Log.ColorUserAccentText("TSM4"))
tab:SetNormalFontObject(GameFontHighlightSmall)
if TSM.IsWowClassic() then
tab:SetPoint("LEFT", _G["AuctionFrameTab"..tabId - 1], "RIGHT", -8, 0)
else
tab:SetPoint("LEFT", AuctionHouseFrame.Tabs[tabId - 1], "RIGHT", -15, 0)
tinsert(AuctionHouseFrame.Tabs, tab)
end
tab:Show()
PanelTemplates_SetNumTabs(private.defaultFrame, tabId)
PanelTemplates_EnableTab(private.defaultFrame, tabId)
ScriptWrapper.Set(tab, "OnClick", private.TSMTabOnClick)
end
if private.settings.showDefault then
UIParent_OnEvent(UIParent, "AUCTION_HOUSE_SHOW")
else
PlaySound(SOUNDKIT.AUCTION_WINDOW_OPEN)
private.ShowAuctionFrame()
end
end
function private.ShowAuctionFrame()
if private.frame then
return
end
private.frame = private.CreateMainFrame()
private.frame:Show()
private.frame:Draw()
for _, callback in ipairs(private.updateCallbacks) do
callback()
end
end
function private.HideAuctionFrame()
if not private.frame then
return
end
private.frame:Hide()
assert(not private.frame)
for _, callback in ipairs(private.updateCallbacks) do
callback()
end
end
function private.CreateMainFrame()
TSM.UI.AnalyticsRecordPathChange("auction")
local frame = UIElements.New("LargeApplicationFrame", "base")
:SetParent(UIParent)
:SetSettingsContext(private.settings, "frame")
:SetMinResize(MIN_FRAME_SIZE.width, MIN_FRAME_SIZE.height)
:SetStrata("HIGH")
:SetProtected(TSM.db.global.coreOptions.protectAuctionHouse)
:AddPlayerGold()
:AddAppStatusIcon()
:AddSwitchButton(private.SwitchBtnOnClick)
:SetScript("OnHide", private.BaseFrameOnHide)
for _, info in ipairs(private.topLevelPages) do
frame:AddNavButton(info.name, info.callback)
end
local whatsNewDialog = TSM.UI.WhatsNew.GetDialog()
if whatsNewDialog then
frame:ShowDialogFrame(whatsNewDialog)
end
return frame
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.BaseFrameOnHide(frame)
assert(frame == private.frame)
frame:Release()
private.frame = nil
if not private.isSwitching then
PlaySound(SOUNDKIT.AUCTION_WINDOW_CLOSE)
if TSM.IsWowClassic() then
CloseAuctionHouse()
else
C_AuctionHouse.CloseAuctionHouse()
end
end
TSM.UI.AnalyticsRecordClose("auction")
end
function private.SwitchBtnOnClick(button)
private.isSwitching = true
private.settings.showDefault = true
private.HideAuctionFrame()
UIParent_OnEvent(UIParent, "AUCTION_HOUSE_SHOW")
private.isSwitching = false
end
local function NoOp()
-- do nothing - what did you expect?
end
function private.TSMTabOnClick()
private.settings.showDefault = false
if TSM.IsWowClassic() then
ClearCursor()
ClickAuctionSellItemButton(AuctionsItemButton, "LeftButton")
end
ClearCursor()
-- Replace CloseAuctionHouse() with a no-op while hiding the AH frame so we don't stop interacting with the AH NPC
if TSM.IsWowClassic() then
local origCloseAuctionHouse = CloseAuctionHouse
CloseAuctionHouse = NoOp
AuctionFrame_Hide()
CloseAuctionHouse = origCloseAuctionHouse
else
local origCloseAuctionHouse = C_AuctionHouse.CloseAuctionHouse
C_AuctionHouse.CloseAuctionHouse = NoOp
HideUIPanel(private.defaultFrame)
C_AuctionHouse.CloseAuctionHouse = origCloseAuctionHouse
end
private.ShowAuctionFrame()
end
function private.ItemLinkedCallback(name, itemLink)
if not private.frame then
return
end
local path = private.frame:GetSelectedNavButton()
for _, info in ipairs(private.topLevelPages) do
if info.name == path then
if info.itemLinkedHandler(name, itemLink) then
return true
else
return
end
end
end
error("Invalid frame path")
end

View File

@@ -0,0 +1,715 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local MyAuctions = TSM.UI.AuctionUI:NewPackage("MyAuctions")
local L = TSM.Include("Locale").GetTable()
local FSM = TSM.Include("Util.FSM")
local Money = TSM.Include("Util.Money")
local String = TSM.Include("Util.String")
local Math = TSM.Include("Util.Math")
local TempTable = TSM.Include("Util.TempTable")
local ItemString = TSM.Include("Util.ItemString")
local Theme = TSM.Include("Util.Theme")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
local private = {
settings = nil,
fsm = nil,
frame = nil,
query = nil,
}
local DURATION_LIST = {
L["None"],
AUCTION_TIME_LEFT1_DETAIL,
AUCTION_TIME_LEFT2_DETAIL,
AUCTION_TIME_LEFT3_DETAIL,
AUCTION_TIME_LEFT4_DETAIL,
}
local SECONDS_PER_MIN = 60
local SECONDS_PER_HOUR = 60 * SECONDS_PER_MIN
-- ============================================================================
-- Module Functions
-- ============================================================================
function MyAuctions.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "auctionUIContext", "myAuctionsScrollingTable")
private.FSMCreate()
TSM.UI.AuctionUI.RegisterTopLevelPage(L["My Auctions"], private.GetMyAuctionsFrame, private.OnItemLinked)
end
-- ============================================================================
-- MyAuctions UI
-- ============================================================================
function private.GetMyAuctionsFrame()
TSM.UI.AnalyticsRecordPathChange("auction", "my_auctions")
private.query = private.query or TSM.MyAuctions.CreateQuery()
local frame = UIElements.New("Frame", "myAuctions")
:SetLayout("VERTICAL")
:SetBackgroundColor("PRIMARY_BG_ALT")
:AddChild(UIElements.New("Frame", "header")
:SetLayout("VERTICAL")
:SetHeight(72)
:SetPadding(8)
:AddChild(UIElements.New("Frame", "filters")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(0, 0, 0, 8)
:AddChild(UIElements.New("SelectionDropdown", "duration")
:SetWidth(232)
:SetItems(DURATION_LIST)
:SetHintText(L["Filter by duration"])
:SetScript("OnSelectionChanged", private.DurationFilterChanged)
)
:AddChild(UIElements.New("GroupSelector", "group")
:SetWidth(232)
:SetMargin(8, 8, 0, 0)
:SetHintText(L["Filter by groups"])
:SetScript("OnSelectionChanged", private.FilterChanged)
)
:AddChild(UIElements.New("Input", "keyword")
:SetIconTexture("iconPack.18x18/Search")
:AllowItemInsert(false)
:SetClearButtonEnabled(true)
:SetHintText(L["Filter by keyword"])
:SetScript("OnValueChanged", private.FilterChanged)
)
)
:AddChild(UIElements.New("Frame", "buttons")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Checkbox", "ignoreBid")
:SetWidth("AUTO")
:SetCheckboxPosition("LEFT")
:SetText(L["Hide auctions with bids"])
:SetScript("OnValueChanged", private.ToggleFilterChanged)
)
:AddChild(UIElements.New("Checkbox", "onlyBid")
:SetMargin(16, 0, 0, 0)
:SetWidth("AUTO")
:SetCheckboxPosition("LEFT")
:SetText(L["Show only auctions with bids"])
:SetScript("OnValueChanged", private.ToggleFilterChanged)
)
:AddChild(UIElements.New("Checkbox", "onlySold")
:SetMargin(16, 0, 0, 0)
:SetWidth("AUTO")
:SetCheckboxPosition("LEFT")
:SetText(L["Only show sold auctions"])
:SetScript("OnValueChanged", private.ToggleFilterChanged)
)
:AddChild(UIElements.New("Spacer"))
:AddChild(UIElements.New("ActionButton", "clearfilter")
:SetSize(142, 24)
:SetText(L["Clear Filters"])
:SetScript("OnClick", private.ClearFilterOnClick)
)
)
)
if TSM.IsWowClassic() then
frame:AddChild(UIElements.New("MyAuctionsScrollingTable", "auctions")
:SetSettingsContext(private.settings, "myAuctionsScrollingTable")
:GetScrollingTableInfo()
:NewColumn("item")
:SetTitle(L["Item Name"])
:SetIconSize(12)
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo(nil, private.AuctionsGetItemText)
:SetIconInfo("itemString", ItemInfo.GetTexture)
:SetTooltipInfo("itemString", private.AuctionsGetItemTooltip)
:SetActionIconInfo(1, 12, private.AuctionsGetSoldIcon)
:DisableHiding()
:Commit()
:NewColumn("stackSize")
:SetTitle(L["Qty"])
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextInfo(nil, private.AuctionsGetStackSizeText)
:Commit()
:NewColumn("timeLeft")
:SetTitleIcon("iconPack.14x14/Clock")
:SetFont("BODY_BODY3")
:SetJustifyH("CENTER")
:SetTextInfo(nil, private.AuctionsGetTimeLeftText)
:Commit()
:NewColumn("highbidder")
:SetTitle(L["High Bidder"])
:SetFont("BODY_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo(nil, private.AuctionsGetHighBidderText)
:Commit()
:NewColumn("group")
:SetTitle(GROUP)
:SetFont("BODY_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo(nil, private.AuctionsGetGroupNameText)
:Commit()
:NewColumn("currentBid")
:SetTitle(BID)
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextInfo(nil, private.AuctionsGetCurrentBidText)
:Commit()
:NewColumn("buyout")
:SetTitle(BUYOUT)
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextInfo(nil, private.AuctionsGetCurrentBuyoutText)
:Commit()
:Commit()
:SetQuery(private.query)
:SetSelectionValidator(private.AuctionsValidateSelection)
:SetScript("OnSelectionChanged", private.AuctionsOnSelectionChanged)
:SetScript("OnDataUpdated", private.AuctionsOnDataUpdated)
)
else
frame:AddChild(UIElements.New("MyAuctionsScrollingTable", "auctions")
:SetSettingsContext(private.settings, "myAuctionsScrollingTable")
:GetScrollingTableInfo()
:NewColumn("item")
:SetTitle(L["Item Name"])
:SetIconSize(12)
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo(nil, private.AuctionsGetItemText)
:SetIconInfo("itemString", ItemInfo.GetTexture)
:SetTooltipInfo("itemString", private.AuctionsGetItemTooltip)
:SetActionIconInfo(1, 12, private.AuctionsGetSoldIcon)
:SetSortInfo("name")
:DisableHiding()
:Commit()
:NewColumn("stackSize")
:SetTitle(L["Qty"])
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextInfo(nil, private.AuctionsGetStackSizeText)
:SetSortInfo("stackSize")
:Commit()
:NewColumn("timeLeft")
:SetTitleIcon("iconPack.14x14/Clock")
:SetFont("BODY_BODY3")
:SetJustifyH("CENTER")
:SetTextInfo(nil, private.AuctionsGetTimeLeftText)
:SetSortInfo("duration")
:Commit()
:NewColumn("group")
:SetTitle(GROUP)
:SetFont("BODY_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo(nil, private.AuctionsGetGroupNameText)
:SetSortInfo("group")
:Commit()
:NewColumn("currentBid")
:SetTitle(BID)
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextInfo(nil, private.AuctionsGetCurrentBidText)
:SetSortInfo("currentBid")
:Commit()
:NewColumn("buyout")
:SetTitle(BUYOUT)
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextInfo(nil, private.AuctionsGetCurrentBuyoutText)
:SetSortInfo("buyout")
:Commit()
:Commit()
:SetQuery(private.query)
:SetSelectionValidator(private.AuctionsValidateSelection)
:SetScript("OnSelectionChanged", private.AuctionsOnSelectionChanged)
:SetScript("OnDataUpdated", private.AuctionsOnDataUpdated)
)
end
frame:AddChild(UIElements.New("Texture", "line")
:SetHeight(2)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Frame", "bottom")
:SetLayout("VERTICAL")
:SetHeight(68)
:SetPadding(8)
:SetBackgroundColor("PRIMARY_BG_ALT")
:AddChild(UIElements.New("Frame", "row1")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 0, 8)
:AddChild(UIElements.New("Text", "sold")
:SetWidth("AUTO")
:SetFont("BODY_BODY3_MEDIUM")
)
:AddChild(UIElements.New("Text", "soldValue")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
)
:AddChild(UIElements.New("Texture", "vline")
:SetWidth(1)
:SetMargin(8, 8, 0, 0)
:SetTexture("ACTIVE_BG_ALT")
)
:AddChild(UIElements.New("Text", "posted")
:SetWidth("AUTO")
:SetFont("BODY_BODY3_MEDIUM")
)
:AddChild(UIElements.New("Text", "postedValue")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
)
:AddChild(UIElements.New("Spacer", "spacer"))
)
:AddChild(UIElements.New("Frame", "row2")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("ProgressBar", "progressBar")
:SetMargin(0, 8, 0, 0)
:SetProgress(0)
)
:AddChild(UIElements.NewNamed("ActionButton", "cancelBtn", "TSMCancelAuctionBtn")
:SetSize(110, 24)
:SetMargin(0, 8, 0, 0)
:SetText(CANCEL)
:SetDisabled(true)
:DisableClickCooldown(true)
:SetScript("OnClick", private.CancelButtonOnClick)
)
:AddChild(UIElements.New("ActionButton", "skipBtn")
:SetSize(110, 24)
:SetText(L["Skip"])
:SetDisabled(true)
:DisableClickCooldown(true)
:SetScript("OnClick", private.SkipButtonOnClick)
)
)
)
:SetScript("OnUpdate", private.FrameOnUpdate)
:SetScript("OnHide", private.FrameOnHide)
private.frame = frame
return frame
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnItemLinked(name)
private.frame:GetElement("header.filters.keyword")
:SetValue(name)
:Draw()
private.FilterChanged()
return true
end
function private.FrameOnUpdate(frame)
frame:SetScript("OnUpdate", nil)
private.fsm:ProcessEvent("EV_FRAME_SHOWN", frame)
end
function private.FrameOnHide(frame)
assert(frame == private.frame)
private.frame = nil
private.fsm:ProcessEvent("EV_FRAME_HIDDEN")
end
function private.ToggleFilterChanged(toggle)
local ignoreBidToggle = toggle:GetElement("__parent.ignoreBid")
local onlyBidToggle = toggle:GetElement("__parent.onlyBid")
local onlySoldToggle = toggle:GetElement("__parent.onlySold")
if ignoreBidToggle:IsChecked() and onlyBidToggle:IsChecked() then
-- uncheck the other toggle in the pair of "bid" toggles
if toggle == ignoreBidToggle then
onlyBidToggle:SetChecked(false, true)
:Draw()
else
ignoreBidToggle:SetChecked(false, true)
:Draw()
end
end
if onlyBidToggle:IsChecked() and onlySoldToggle:IsChecked() then
-- uncheck the other toggle in the pair of "only" toggles
if toggle == onlyBidToggle then
onlySoldToggle:SetChecked(false, true)
:Draw()
else
onlyBidToggle:SetChecked(false, true)
:Draw()
end
end
private.FilterChanged()
end
function private.DurationFilterChanged(dropdown)
if dropdown:GetSelectedItemKey() == 1 then
-- none
dropdown:SetSelectedItem(nil, true)
end
private.FilterChanged()
end
function private.FilterChanged()
private.fsm:ProcessEvent("EV_FILTER_UPDATED")
end
function private.ClearFilterOnClick(button)
button:GetElement("__parent.__parent.filters.duration")
:SetOpen(false)
:SetSelectedItem()
button:GetElement("__parent.__parent.filters.group")
:ClearSelectedGroups()
button:GetElement("__parent.__parent.filters.keyword")
:SetValue("")
:SetFocused(false)
button:GetElement("__parent.ignoreBid")
:SetChecked(false, true)
button:GetElement("__parent.onlyBid")
:SetChecked(false, true)
button:GetElement("__parent.onlySold")
:SetChecked(false, true)
button:GetParentElement():GetParentElement():Draw()
private.fsm:ProcessEvent("EV_FILTER_UPDATED")
end
function private.AuctionsValidateSelection(_, row)
return row:GetField("saleStatus") == 0
end
function private.AuctionsOnSelectionChanged()
private.fsm:ProcessEvent("EV_SELECTION_CHANGED")
end
function private.AuctionsOnDataUpdated()
if not private.frame then
return
end
private.fsm:SetLoggingEnabled(false)
private.fsm:ProcessEvent("EV_DATA_UPDATED")
private.fsm:SetLoggingEnabled(true)
end
function private.CancelButtonOnClick(button)
private.fsm:ProcessEvent("EV_CANCEL_CLICKED")
end
function private.SkipButtonOnClick(button)
private.fsm:ProcessEvent("EV_SKIP_CLICKED")
end
-- ============================================================================
-- FSM
-- ============================================================================
function private.FSMCreate()
local fsmContext = {
frame = nil,
durationFilter = nil,
keywordFilter = nil,
groupFilter = {},
bidFilter = nil,
soldFilter = false,
filterChanged = false,
}
local function UpdateFrame(context)
if not context.frame then
return
end
local auctions = context.frame:GetElement("auctions")
if context.filterChanged then
context.filterChanged = false
private.query:ResetFilters()
if context.durationFilter then
if TSM.IsWowClassic() then
private.query:Equal("duration", context.durationFilter)
else
if context.durationFilter == 1 then
private.query:LessThan("duration", time() + (30 * SECONDS_PER_MIN))
elseif context.durationFilter == 2 then
private.query:LessThan("duration", time() + (2 * SECONDS_PER_HOUR))
elseif context.durationFilter == 3 then
private.query:LessThanOrEqual("duration", time() + (12 * SECONDS_PER_HOUR))
else
private.query:GreaterThan("duration", time() + (12 * SECONDS_PER_HOUR))
end
end
end
if context.keywordFilter then
private.query:Matches("itemName", context.keywordFilter)
end
if next(context.groupFilter) then
private.query:InTable("group", context.groupFilter)
end
if context.bidFilter ~= nil then
if context.bidFilter then
private.query:NotEqual("highBidder", "")
else
private.query:Equal("highBidder", "")
end
end
if context.soldFilter then
private.query:Equal("saleStatus", 1)
end
auctions:SetQuery(private.query, true)
end
local selectedRow = auctions:GetSelection()
local hasFilter = context.durationFilter or context.keywordFilter or next(context.groupFilter) or context.bidFilter ~= nil or context.soldFilter
context.frame:GetElement("header.buttons.clearfilter")
:SetDisabled(not hasFilter)
:Draw()
local numPending = TSM.MyAuctions.GetNumPending()
local progressText = nil
if numPending > 0 then
progressText = format(L["Canceling %d Auctions..."], numPending)
elseif selectedRow then
progressText = L["Ready to Cancel"]
else
progressText = L["Select Auction to Cancel"]
end
local row2 = context.frame:GetElement("bottom.row2")
row2:GetElement("cancelBtn")
:SetDisabled(not selectedRow or (not TSM.IsWowClassic() and numPending > 0) or (not TSM.IsWowClassic() and C_AuctionHouse.GetCancelCost(selectedRow:GetField("auctionId")) > GetMoney()))
row2:GetElement("skipBtn")
:SetDisabled(not selectedRow)
row2:GetElement("progressBar")
:SetProgressIconHidden(numPending == 0)
:SetText(progressText)
row2:Draw()
local numPosted, numSold, postedGold, soldGold = 0, 0, 0, 0
for _, row in private.query:Iterator() do
local itemString, saleStatus, buyout, currentBid, stackSize = row:GetFields("itemString", "saleStatus", "buyout", "currentBid", "stackSize")
if saleStatus == 1 then
numSold = numSold + 1
-- if somebody did a buyout, then bid will be equal to buyout, otherwise it'll be the winning bid
soldGold = soldGold + currentBid
else
numPosted = numPosted + 1
if ItemInfo.IsCommodity(itemString) then
postedGold = postedGold + (buyout * stackSize)
else
postedGold = postedGold + buyout
end
end
end
local row1 = context.frame:GetElement("bottom.row1")
row1:GetElement("sold")
:SetFormattedText((hasFilter and L["%s Sold Auctions (Filtered)"] or L["%s Sold Auctions"])..":", Theme.GetColor("INDICATOR"):ColorText(numSold))
row1:GetElement("soldValue")
:SetText(Money.ToString(soldGold))
row1:GetElement("posted")
:SetFormattedText((hasFilter and L["%s Posted Auctions (Filtered)"] or L["%s Posted Auctions"])..":", Theme.GetColor("INDICATOR_ALT"):ColorText(numPosted))
row1:GetElement("postedValue")
:SetText(Money.ToString(postedGold))
row1:Draw()
end
private.fsm = FSM.New("MY_AUCTIONS")
:AddState(FSM.NewState("ST_HIDDEN")
:SetOnEnter(function(context)
context.frame = nil
context.durationFilter = nil
context.keywordFilter = nil
wipe(context.groupFilter)
context.bidFilter = nil
context.soldFilter = false
end)
:AddTransition("ST_HIDDEN")
:AddTransition("ST_SHOWNING")
:AddEventTransition("EV_FRAME_SHOWN", "ST_SHOWNING")
)
:AddState(FSM.NewState("ST_SHOWNING")
:SetOnEnter(function(context, frame)
context.frame = frame
context.filterChanged = true
return "ST_SHOWN"
end)
:AddTransition("ST_SHOWN")
)
:AddState(FSM.NewState("ST_SHOWN")
:SetOnEnter(function(context)
UpdateFrame(context)
end)
:AddTransition("ST_HIDDEN")
:AddTransition("ST_SHOWN")
:AddTransition("ST_CANCELING")
:AddEventTransition("EV_CANCEL_CLICKED", "ST_CANCELING")
:AddEventTransition("EV_SELECTION_CHANGED", "ST_SHOWN")
:AddEventTransition("EV_DATA_UPDATED", "ST_SHOWN")
:AddEvent("EV_SKIP_CLICKED", function(context)
context.frame:GetElement("auctions"):SelectNextRow()
return "ST_SHOWN"
end)
:AddEvent("EV_FILTER_UPDATED", function(context)
local didChange = false
local durationFilter = context.frame:GetElement("header.filters.duration"):GetSelectedItemKey()
durationFilter = durationFilter and (durationFilter - 1) or nil
if durationFilter ~= context.durationFilter then
context.durationFilter = durationFilter
didChange = true
end
local keywordFilter = context.frame:GetElement("header.filters.keyword"):GetValue()
keywordFilter = keywordFilter ~= "" and String.Escape(keywordFilter) or nil
if keywordFilter ~= context.keywordFilter then
context.keywordFilter = keywordFilter
didChange = true
end
local newGroupFilter = TempTable.Acquire()
for groupPath in context.frame:GetElement("header.filters.group"):SelectedGroupIterator() do
newGroupFilter[groupPath] = true
end
if Math.CalculateHash(newGroupFilter) ~= Math.CalculateHash(context.groupFilter) then
didChange = true
wipe(context.groupFilter)
for groupPath in pairs(newGroupFilter) do
context.groupFilter[groupPath] = true
end
end
TempTable.Release(newGroupFilter)
local bidFilter = nil
if context.frame:GetElement("header.buttons.ignoreBid"):IsChecked() then
bidFilter = false
elseif context.frame:GetElement("header.buttons.onlyBid"):IsChecked() then
bidFilter = true
end
if bidFilter ~= context.bidFilter then
context.bidFilter = bidFilter
didChange = true
end
local soldFilter = context.frame:GetElement("header.buttons.onlySold"):IsChecked()
if soldFilter ~= context.soldFilter then
context.soldFilter = soldFilter
didChange = true
end
if didChange then
context.filterChanged = true
return "ST_SHOWN"
end
end)
)
:AddState(FSM.NewState("ST_CANCELING")
:SetOnEnter(function(context)
local buttonsFrame = context.frame:GetElement("bottom")
buttonsFrame:GetElement("row2.cancelBtn"):SetDisabled(true)
buttonsFrame:GetElement("row2.skipBtn"):SetDisabled(true)
buttonsFrame:Draw()
local auctionId = context.frame:GetElement("auctions"):GetSelection():GetField("auctionId")
if TSM.MyAuctions.CanCancel(auctionId) then
TSM.MyAuctions.CancelAuction(auctionId)
end
return "ST_SHOWN"
end)
:AddTransition("ST_HIDDEN")
:AddTransition("ST_SHOWN")
)
:AddDefaultEventTransition("EV_FRAME_HIDDEN", "ST_HIDDEN")
:Init("ST_HIDDEN", fsmContext)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.AuctionsGetItemText(row)
if row:GetField("saleStatus") == 1 then
return Theme.GetColor("INDICATOR"):ColorText(row:GetField("itemName"))
else
return TSM.UI.GetQualityColoredText(row:GetField("itemName"), row:GetField("itemQuality"))
end
end
function private.AuctionsGetItemTooltip(itemString)
return itemString ~= ItemString.GetPetCage() and itemString or nil
end
function private.AuctionsGetSoldIcon(self, data, iconIndex)
assert(iconIndex == 1)
local row = private.query:GetResultRowByUUID(data)
if row:GetField("saleStatus") ~= 1 then
return
end
return true, "iconPack.12x12/Bid", true
end
function private.AuctionsGetStackSizeText(row)
if row:GetField("saleStatus") == 1 then
return Theme.GetColor("INDICATOR"):ColorText(row:GetField("stackSize"))
else
return row:GetField("stackSize")
end
end
function private.AuctionsGetTimeLeftText(row)
local saleStatus, duration, isPending = row:GetFields("saleStatus", "duration", "isPending")
if saleStatus == 0 and isPending then
return "..."
elseif saleStatus == 1 or not TSM.IsWowClassic() then
local timeLeft = duration - time()
local color = nil
if saleStatus == 0 then
if timeLeft <= 2 * SECONDS_PER_HOUR then
color = Theme.GetFeedbackColor("RED")
elseif timeLeft <= (TSM.IsWowClassic() and 8 or 24) * SECONDS_PER_HOUR then
color = Theme.GetFeedbackColor("YELLOW")
else
color = Theme.GetFeedbackColor("GREEN")
end
else
color = Theme.GetColor("INDICATOR")
end
local str = nil
if timeLeft < SECONDS_PER_MIN then
str = timeLeft.."s"
elseif timeLeft < SECONDS_PER_HOUR then
str = floor(timeLeft / SECONDS_PER_MIN).."m"
else
str = floor(timeLeft / SECONDS_PER_HOUR).."h"
end
return color and color:ColorText(str) or str
else
return TSM.UI.GetTimeLeftString(duration)
end
end
function private.AuctionsGetHighBidderText(row)
if row:GetField("saleStatus") == 1 then
return Theme.GetColor("INDICATOR"):ColorText(row:GetField("highBidder"))
else
return row:GetField("highBidder")
end
end
function private.AuctionsGetGroupNameText(row)
local groupPath = row:GetField("group")
local groupName = TSM.Groups.Path.GetName(groupPath)
local level = select('#', strsplit(TSM.CONST.GROUP_SEP, groupPath))
return Theme.GetGroupColor(level):ColorText(groupName)
end
function private.AuctionsGetCurrentBidText(row)
if row:GetField("saleStatus") == 1 then
return Theme.GetColor("INDICATOR"):ColorText(L["Sold for:"])
elseif not TSM.IsWowClassic() and row:GetField("highBidder") ~= "" then
return Money.ToString(row:GetField("currentBid"), Theme.GetColor("INDICATOR"):GetTextColorPrefix())
else
return Money.ToString(row:GetField("currentBid"))
end
end
function private.AuctionsGetCurrentBuyoutText(row)
return Money.ToString(row:GetField(row:GetField("saleStatus") == 1 and "currentBid" or "buyout"))
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,818 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Sniper = TSM.UI.AuctionUI:NewPackage("Sniper")
local L = TSM.Include("Locale").GetTable()
local Delay = TSM.Include("Util.Delay")
local Event = TSM.Include("Util.Event")
local FSM = TSM.Include("Util.FSM")
local Sound = TSM.Include("Util.Sound")
local Money = TSM.Include("Util.Money")
local Log = TSM.Include("Util.Log")
local ItemString = TSM.Include("Util.ItemString")
local ItemInfo = TSM.Include("Service.ItemInfo")
local AuctionScan = TSM.Include("Service.AuctionScan")
local MailTracking = TSM.Include("Service.MailTracking")
local Settings = TSM.Include("Service.Settings")
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
local PlayerInfo = TSM.Include("Service.PlayerInfo")
local UIElements = TSM.Include("UI.UIElements")
local private = {
settings = nil,
fsm = nil,
selectionFrame = nil,
hasLastScan = nil,
contentPath = "selection",
}
local PHASED_TIME = 60
local RETAIL_RESCAN_DELAY = 30
-- ============================================================================
-- Module Functions
-- ============================================================================
function Sniper.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "auctionUIContext", "sniperScrollingTable")
:AddKey("global", "sniperOptions", "sniperSound")
TSM.UI.AuctionUI.RegisterTopLevelPage(L["Sniper"], private.GetSniperFrame, private.OnItemLinked)
private.FSMCreate()
end
-- ============================================================================
-- Sniper UI
-- ============================================================================
function private.GetSniperFrame()
TSM.UI.AnalyticsRecordPathChange("auction", "sniper")
if not private.hasLastScan then
private.contentPath = "selection"
end
return UIElements.New("ViewContainer", "sniper")
:SetNavCallback(private.GetSniperContentFrame)
:AddPath("selection")
:AddPath("scan")
:SetPath(private.contentPath)
end
function private.GetSniperContentFrame(viewContainer, path)
private.contentPath = path
if path == "selection" then
return private.GetSelectionFrame()
elseif path == "scan" then
return private.GetScanFrame()
else
error("Unexpected path: "..tostring(path))
end
end
function private.GetSelectionFrame()
TSM.UI.AnalyticsRecordPathChange("auction", "sniper", "selection")
local frame = UIElements.New("Frame", "selection")
:SetLayout("VERTICAL")
:SetBackgroundColor("PRIMARY_BG_ALT")
:AddChildIf(TSM.IsWowClassic(), UIElements.New("Text", "text")
:SetHeight(20)
:SetMargin(8, 8, 12, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetJustifyH("CENTER")
:SetText(L["Start either a 'Buyout' or 'Bid' sniper using the buttons above."])
)
:AddChild(UIElements.New("Frame", "buttons")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(8, 8, 12, 12)
:AddChild(UIElements.New("ActionButton", "buyoutScanBtn")
:SetMargin(0, TSM.IsWowClassic() and 8 or 0, 0, 0)
:SetText(L["Run Buyout Sniper"])
:SetScript("OnClick", private.BuyoutScanButtonOnClick)
)
:AddChildIf(TSM.IsWowClassic(), UIElements.New("ActionButton", "bidScanBtn")
:SetText(L["Run Bid Sniper"])
:SetScript("OnClick", private.BidScanButtonOnClick)
)
)
:AddChild(UIElements.New("SniperScrollingTable", "auctions")
:SetSettingsContext(private.settings, "sniperScrollingTable")
)
:SetScript("OnHide", private.SelectionFrameOnHide)
private.selectionFrame = frame
return frame
end
function private.GetScanFrame()
TSM.UI.AnalyticsRecordPathChange("auction", "sniper", "scan")
return UIElements.New("Frame", "scan")
:SetLayout("VERTICAL")
:SetBackgroundColor("PRIMARY_BG_ALT")
:AddChild(UIElements.New("Frame", "header")
:SetLayout("HORIZONTAL")
:SetHeight(48)
:SetPadding(8, 8, 14, 14)
:AddChild(UIElements.New("ActionButton", "backBtn")
:SetSize(64, 24)
:SetMargin(0, 16, 0, 0)
:SetIcon("iconPack.14x14/Chevron/Right@180")
:SetText(BACK)
:SetScript("OnClick", private.BackButtonOnClick)
)
:AddChild(UIElements.New("Text", "title")
:SetFont("BODY_BODY2_MEDIUM")
:SetJustifyH("CENTER")
)
:AddChild(UIElements.New("ActionButton", "restartBtn")
:SetSize(80, 24)
:SetText(L["Restart"])
:SetScript("OnClick", private.RestartButtonOnClick)
)
)
:AddChild(UIElements.New("SniperScrollingTable", "auctions")
:SetSettingsContext(private.settings, "sniperScrollingTable")
:SetScript("OnSelectionChanged", private.AuctionsOnSelectionChanged)
:SetScript("OnRowRemoved", private.AuctionsOnRowRemoved)
)
:AddChild(UIElements.New("Texture", "line")
:SetHeight(2)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Frame", "bottom")
:SetLayout("HORIZONTAL")
:SetHeight(40)
:SetPadding(8)
:SetBackgroundColor("PRIMARY_BG_ALT")
:AddChild(UIElements.New("ActionButton", "pauseResumeBtn")
:SetSize(24, 24)
:SetMargin(0, 8, 0, 0)
:SetIcon("iconPack.18x18/PlayPause")
:SetScript("OnClick", private.PauseResumeBtnOnClick)
)
:AddChild(UIElements.New("ProgressBar", "progressBar")
:SetHeight(24)
:SetMargin(0, 8, 0, 0)
:SetProgress(0)
:SetText(L["Starting Scan..."])
)
:AddChild(UIElements.NewNamed("ActionButton", "actionBtn", "TSMSniperBtn")
:SetSize(165, 24)
:SetText(BID)
:SetDisabled(true)
:DisableClickCooldown(true)
:SetScript("OnClick", private.ActionButtonOnClick)
)
)
:SetScript("OnUpdate", private.ScanFrameOnUpdate)
:SetScript("OnHide", private.ScanFrameOnHide)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnItemLinked(name, itemLink)
if private.selectionFrame then
return false
end
private.fsm:ProcessEvent("EV_STOP_CLICKED")
TSM.UI.AuctionUI.SetOpenPage(L["Browse"])
TSM.UI.AuctionUI.Shopping.StartItemSearch(itemLink)
return true
end
function private.SelectionFrameOnHide(frame)
assert(frame == private.selectionFrame)
private.selectionFrame = nil
end
function private.StartScanHelper(viewContainer, searchContext)
if not TSM.UI.AuctionUI.StartingScan(L["Sniper"]) then
return
end
viewContainer:SetPath("scan", true)
private.fsm:ProcessEvent("EV_START_SCAN", searchContext)
end
function private.BuyoutScanButtonOnClick(button)
local viewContainer = button:GetParentElement():GetParentElement():GetParentElement()
local searchContext = TSM.Sniper.BuyoutSearch.GetSearchContext()
private.StartScanHelper(viewContainer, searchContext)
end
function private.BidScanButtonOnClick(button)
local viewContainer = button:GetParentElement():GetParentElement():GetParentElement()
local searchContext = TSM.Sniper.BidSearch.GetSearchContext()
private.StartScanHelper(viewContainer, searchContext)
end
function private.AuctionsOnSelectionChanged()
private.fsm:ProcessEvent("EV_AUCTION_SELECTION_CHANGED")
end
function private.AuctionsOnRowRemoved(_, row)
private.fsm:ProcessEvent("EV_AUCTION_ROW_REMOVED", row)
end
function private.BackButtonOnClick()
private.fsm:ProcessEvent("EV_STOP_CLICKED")
end
function private.PauseResumeBtnOnClick(button)
private.fsm:ProcessEvent("EV_PAUSE_RESUME_CLICKED")
end
function private.ActionButtonOnClick(button)
private.fsm:ProcessEvent("EV_ACTION_CLICKED")
end
function private.RestartButtonOnClick(button)
if not TSM.UI.AuctionUI.StartingScan(L["Sniper"]) then
return
end
local lastScanType = private.hasLastScan
local sniperFrame = button:GetParentElement():GetParentElement():GetParentElement()
private.fsm:ProcessEvent("EV_STOP_CLICKED")
if lastScanType == "bid" then
sniperFrame:GetElement("selection.buttons.bidScanBtn"):Click()
elseif lastScanType == "buyout" then
sniperFrame:GetElement("selection.buttons.buyoutScanBtn"):Click()
else
error("Invalid last scan type: "..tostring(lastScanType))
end
end
function private.ScanFrameOnUpdate(frame)
frame:SetScript("OnUpdate", nil)
private.fsm:ProcessEvent("EV_SCAN_FRAME_SHOWN", frame)
end
function private.ScanFrameOnHide(frame)
private.fsm:ProcessEvent("EV_SCAN_FRAME_HIDDEN")
end
-- ============================================================================
-- FSM
-- ============================================================================
function private.FSMCreate()
local fsmContext = {
scanFrame = nil,
auctionScan = nil,
query = nil,
progress = 0,
progressText = L["Running Sniper Scan"],
buttonsDisabled = true,
findHash = nil,
findAuction = nil,
findResult = nil,
numFound = 0,
maxQuantity = 0,
numActioned = 0,
lastBuyQuantity = 0,
numConfirmed = 0,
searchContext = nil,
scanDone = false,
}
Event.Register("CHAT_MSG_SYSTEM", private.FSMMessageEventHandler)
Event.Register("UI_ERROR_MESSAGE", private.FSMMessageEventHandler)
if not TSM.IsWowClassic() then
Event.Register("COMMODITY_PURCHASE_SUCCEEDED", private.FSMBuyoutSuccess)
end
Event.Register("AUCTION_HOUSE_CLOSED", function()
private.fsm:ProcessEvent("EV_AUCTION_HOUSE_CLOSED")
end)
AuctionHouseWrapper.RegisterAuctionIdUpdateCallback(function(...)
private.fsm:ProcessEvent("EV_AUCTION_ID_UPDATE", ...)
end)
local function UpdateScanFrame(context)
if not context.scanFrame or not context.searchContext then
return
end
local actionText = nil
if context.searchContext:IsBuyoutScan() then
actionText = BUYOUT
elseif context.searchContext:IsBidScan() then
actionText = BID
else
error("Invalid scan type")
end
local bottom = context.scanFrame:GetElement("bottom")
bottom:GetElement("actionBtn")
:SetText(actionText)
:SetDisabled(context.buttonsDisabled)
bottom:GetElement("progressBar")
:SetProgress(context.progress)
:SetText(context.progressText or "")
local auctionList = context.scanFrame:GetElement("auctions")
:SetContext(context.auctionScan)
:SetAuctionScan(context.auctionScan)
:SetMarketValueFunction(context.searchContext:GetMarketValueFunc())
if context.findAuction and not auctionList:GetSelection() then
auctionList:SetSelection(context.findAuction)
end
local title = context.scanFrame:GetElement("header.title")
if context.scanPausing or auctionList:GetSelection() then
if context.searchContext:IsBuyoutScan() then
title:SetText(L["Buyout Sniper Paused"])
elseif context.searchContext:IsBidScan() then
title:SetText(L["Bid Sniper Paused"])
else
error("Invalid scan type")
end
else
if context.searchContext:IsBuyoutScan() then
title:SetText(L["Buyout Sniper Running"])
elseif context.searchContext:IsBidScan() then
title:SetText(L["Bid Sniper Running"])
else
error("Invalid scan type")
end
end
context.scanFrame:Draw()
end
local function ScanOnFilterDone(self, filter, numNewResults)
if numNewResults > 0 then
Sound.PlaySound(private.settings.sniperSound)
end
end
private.fsm = FSM.New("SNIPER")
:AddState(FSM.NewState("ST_INIT")
:SetOnEnter(function(context, searchContext)
private.hasLastScan = nil
if context.searchContext then
context.searchContext:KillThread()
context.searchContext = nil
end
context.progress = 0
context.progressText = L["Running Sniper Scan"]
context.buttonsDisabled = true
context.findHash = nil
context.findAuction = nil
context.findResult = nil
context.numFound = 0
context.numActioned = 0
context.lastBuyQuantity = 0
context.numConfirmed = 0
if context.auctionScan then
context.auctionScan:Release()
context.auctionScan = nil
end
if searchContext then
context.searchContext = searchContext
return "ST_RUNNING_SCAN"
elseif context.scanFrame then
context.scanFrame:GetParentElement():SetPath("selection", true)
context.scanFrame = nil
end
TSM.UI.AuctionUI.EndedScan(L["Sniper"])
end)
:AddTransition("ST_INIT")
:AddTransition("ST_RUNNING_SCAN")
:AddEventTransition("EV_START_SCAN", "ST_INIT")
)
:AddState(FSM.NewState("ST_RUNNING_SCAN")
:SetOnEnter(function(context)
context.scanDone = false
if not context.searchContext then
private.hasLastScan = nil
elseif context.searchContext:IsBuyoutScan() then
private.hasLastScan = "buyout"
elseif context.searchContext:IsBidScan() then
private.hasLastScan = "bid"
else
error("Invalid scan type")
end
if not context.auctionScan then
context.auctionScan = AuctionScan.GetManager()
:SetResolveSellers(false)
:SetScript("OnQueryDone", ScanOnFilterDone)
end
if context.scanFrame then
context.scanFrame:GetElement("bottom.progressBar"):SetProgressIconHidden(false)
end
UpdateScanFrame(context)
context.searchContext:StartThread(private.FSMScanCallback, context.auctionScan)
if TSM.IsWowClassic() then
Delay.AfterTime("sniperPhaseDetect", PHASED_TIME, private.FSMPhasedCallback)
end
end)
:SetOnExit(function(context)
Delay.Cancel("sniperPhaseDetect")
end)
:AddTransition("ST_RESULTS")
:AddTransition("ST_WAITING_FOR_PAUSE")
:AddTransition("ST_FINDING_AUCTION")
:AddTransition("ST_INIT")
:AddEvent("EV_PAUSE_RESUME_CLICKED", function(context)
return "ST_WAITING_FOR_PAUSE"
end)
:AddEvent("EV_SCAN_COMPLETE", function(context)
local selection = context.scanFrame and context.scanFrame:GetElement("auctions"):GetSelection()
if selection and selection:IsSubRow() then
return "ST_FINDING_AUCTION"
else
if TSM.IsWowClassic() then
return "ST_RESULTS"
else
-- wait 30 seconds before rescanning to avoid spamming the server with API calls
context.scanDone = true
Delay.AfterTime("SNIPER_RESCAN_DELAY", RETAIL_RESCAN_DELAY, private.FSMRescanDelayed)
end
end
end)
:AddEventTransition("EV_RESCAN_DELAYED", "ST_RESULTS")
:AddEventTransition("EV_SCAN_FAILED", "ST_INIT")
:AddEvent("EV_PHASED", function()
Log.PrintUser(L["You've been phased which has caused the AH to stop working due to a bug on Blizzard's end. Please close and reopen the AH and restart Sniper."])
return "ST_INIT"
end)
:AddEvent("EV_AUCTION_SELECTION_CHANGED", function(context)
assert(context.scanFrame)
if context.scanFrame:GetElement("auctions"):GetSelection() then
if context.scanDone then
return "ST_RESULTS"
else
-- the user selected something, so cancel the current scan
context.auctionScan:Cancel()
end
end
end)
)
:AddState(FSM.NewState("ST_WAITING_FOR_PAUSE")
:SetOnEnter(function(context)
context.scanPausing = true
context.progressText = L["Scan Paused"]
if context.scanFrame then
context.scanFrame:GetElement("bottom.progressBar"):SetProgressIconHidden(true)
end
UpdateScanFrame(context)
end)
:SetOnExit(function(context)
context.scanPausing = false
context.progressText = L["Running Sniper Scan"]
if context.scanFrame then
context.scanFrame:GetElement("bottom.progressBar"):SetProgressIconHidden(false)
end
UpdateScanFrame(context)
end)
:AddEvent("EV_PAUSE_RESUME_CLICKED", function(context)
return "ST_RESULTS"
end)
:AddTransition("ST_RESULTS")
:AddTransition("ST_INIT")
)
:AddState(FSM.NewState("ST_RESULTS")
:SetOnEnter(function(context)
context.searchContext:KillThread()
context.findAuction = nil
context.findResult = nil
context.numFound = 0
context.numActioned = 0
context.lastBuyQuantity = 0
context.numConfirmed = 0
context.progress = 0
context.progressText = L["Running Sniper Scan"]
context.buttonsDisabled = true
UpdateScanFrame(context)
local selection = context.scanFrame and context.scanFrame:GetElement("auctions"):GetSelection()
if selection and selection:IsSubRow() then
return "ST_FINDING_AUCTION"
else
return "ST_RUNNING_SCAN"
end
end)
:AddTransition("ST_RUNNING_SCAN")
:AddTransition("ST_AUCTION_FOUND")
:AddTransition("ST_FINDING_AUCTION")
:AddTransition("ST_INIT")
)
:AddState(FSM.NewState("ST_FINDING_AUCTION")
:SetOnEnter(function(context)
assert(context.scanFrame)
context.findAuction = context.scanFrame:GetElement("auctions"):GetSelection()
assert(context.findAuction:IsSubRow())
context.findHash = context.findAuction:GetHashes()
context.progress = 0
context.progressText = L["Finding Selected Auction"]
context.buttonsDisabled = true
if context.scanFrame then
context.scanFrame:GetElement("bottom.progressBar"):SetProgressIconHidden(false)
end
UpdateScanFrame(context)
TSM.Shopping.SearchCommon.StartFindAuction(context.auctionScan, context.findAuction, private.FSMFindAuctionCallback, true)
end)
:SetOnExit(function(context)
TSM.Shopping.SearchCommon.StopFindAuction()
end)
:AddTransition("ST_RESULTS")
:AddTransition("ST_FINDING_AUCTION")
:AddTransition("ST_AUCTION_FOUND")
:AddTransition("ST_AUCTION_NOT_FOUND")
:AddTransition("ST_INIT")
:AddEventTransition("EV_AUCTION_FOUND", "ST_AUCTION_FOUND")
:AddEventTransition("EV_AUCTION_NOT_FOUND", "ST_AUCTION_NOT_FOUND")
:AddEvent("EV_AUCTION_SELECTION_CHANGED", function(context)
assert(context.scanFrame)
local selection = context.scanFrame and context.scanFrame:GetElement("auctions"):GetSelection()
if selection and selection:IsSubRow() then
return "ST_FINDING_AUCTION"
else
return "ST_RESULTS"
end
end)
:AddEvent("EV_AUCTION_ROW_REMOVED", function(context, row)
if not row:IsSubRow() then
return
end
local removingFindAuction = context.findAuction == row
row:GetResultRow():RemoveSubRow(row)
context.scanFrame:GetElement("auctions"):UpdateData(true)
if removingFindAuction then
return "ST_RESULTS"
end
end)
:AddEvent("EV_SCAN_FRAME_HIDDEN", function(context)
context.scanFrame = nil
context.findAuction = nil
return "ST_RESULTS"
end)
)
:AddState(FSM.NewState("ST_AUCTION_FOUND")
:SetOnEnter(function(context, result)
local selection = context.scanFrame:GetElement("auctions"):GetSelection()
-- update the selection in case the result rows changed
if context.findHash == selection:GetHashes() then
context.findAuction = selection
end
if TSM.IsWowClassic() then
context.findResult = result
context.numFound = #result
else
local maxCommodity = context.findAuction:IsCommodity() and context.findAuction:GetResultRow():GetMaxQuantities()
local numCanBuy = maxCommodity or result
context.findResult = numCanBuy > 0
context.numFound = numCanBuy
context.maxQuantity = maxCommodity or 1
end
assert(context.numActioned == 0 and context.numConfirmed == 0)
return "ST_BIDDING_BUYING"
end)
:AddTransition("ST_BIDDING_BUYING")
)
:AddState(FSM.NewState("ST_AUCTION_NOT_FOUND")
:SetOnEnter(function(context)
local _, rawLink = context.findAuction:GetLinks()
context.findAuction:GetResultRow():RemoveSubRow(context.findAuction)
Log.PrintfUser(L["Failed to find auction for %s, so removing it from the results."], rawLink)
return "ST_RESULTS"
end)
:AddTransition("ST_RESULTS")
)
:AddState(FSM.NewState("ST_BIDDING_BUYING")
:SetOnEnter(function(context, numToRemove)
if numToRemove then
-- remove the one we just bought
context.findAuction:DecrementQuantity(numToRemove)
context.scanFrame:GetElement("auctions"):UpdateData()
context.findAuction = context.scanFrame and context.scanFrame:GetElement("auctions"):GetSelection()
if context.findAuction and not context.findAuction:IsSubRow() then
context.findAuction = nil
end
end
local selection = context.scanFrame and context.scanFrame:GetElement("auctions"):GetSelection()
if selection and not selection:IsSubRow() then
selection = nil
end
local isPlayer, isHighBidder = false, false
if selection then
assert(selection:IsSubRow())
local ownerStr = selection and selection:GetOwnerInfo() or nil
isPlayer = PlayerInfo.IsPlayer(ownerStr, true, true, true)
isHighBidder = select(4, selection:GetBidInfo())
end
local auctionSelected = selection and context.findHash == selection:GetHashes()
local numCanAction = not auctionSelected and 0 or (context.numFound - context.numActioned)
local numConfirming = context.numActioned - context.numConfirmed
local progressText = nil
local actionFormatStr = nil
if context.searchContext:IsBuyoutScan() then
actionFormatStr = L["Buy %d / %d"]
elseif context.searchContext:IsBidScan() then
actionFormatStr = L["Bid %d / %d"]
else
error("Invalid scan type")
end
if numConfirming == 0 and numCanAction == 0 then
-- we're done bidding/buying and confirming this batch
return "ST_RESULTS"
elseif numConfirming == 0 then
-- we can still bid/buy more
progressText = format(actionFormatStr, context.numActioned + 1, context.numFound)
elseif numCanAction == 0 then
-- we're just confirming
progressText = format(L["Confirming %d / %d"], context.numConfirmed + 1, context.numFound)
else
-- we can bid/buy more while confirming
progressText = format(actionFormatStr.." ("..L["Confirming %d / %d"]..")", context.numActioned + 1, context.numFound, context.numConfirmed + 1, context.numFound)
end
context.progress = context.numConfirmed / context.numFound
context.progressText = L["Scan Paused"].." - "..progressText
if numCanAction == 0 or isPlayer or (not TSM.IsWowClassic() and numConfirming > 0) then
context.buttonsDisabled = true
else
if context.searchContext:IsBuyoutScan() then
context.buttonsDisabled = not AuctionScan.CanBuyout(selection, context.auctionScan)
elseif context.searchContext:IsBidScan() then
context.buttonsDisabled = not AuctionScan.CanBid(selection)
else
error("Invalid scan type")
end
end
if context.scanFrame then
context.scanFrame:GetElement("bottom.progressBar")
:SetProgressIconHidden(context.numConfirmed == context.numActioned)
context.scanFrame:GetElement("bottom.actionBtn")
:SetDisabled(isPlayer or (isHighBidder and context.searchContext:IsBidScan()))
:Draw()
end
UpdateScanFrame(context)
end)
:AddTransition("ST_BID_BUY_CONFIRMATION")
:AddTransition("ST_BIDDING_BUYING")
:AddTransition("ST_PLACING_BID_BUY")
:AddTransition("ST_CONFIRMING_BID_BUY")
:AddTransition("ST_RESULTS")
:AddTransition("ST_INIT")
:AddEvent("EV_PAUSE_RESUME_CLICKED", function(context)
context.scanFrame:GetElement("auctions"):SetSelection(nil)
return "ST_RESULTS"
end)
:AddEventTransition("EV_AUCTION_SELECTION_CHANGED", "ST_RESULTS")
:AddEventTransition("EV_ACTION_CLICKED", "ST_BID_BUY_CONFIRMATION")
:AddEvent("EV_CONFIRMED", function(context, isBuy, quantity)
assert(isBuy == context.searchContext:IsBuyoutScan())
return "ST_PLACING_BID_BUY", quantity
end)
:AddEvent("EV_MSG", function(context, msg)
local _, rawLink = context.findAuction:GetLinks()
if msg == LE_GAME_ERR_AUCTION_HIGHER_BID or msg == LE_GAME_ERR_ITEM_NOT_FOUND or msg == LE_GAME_ERR_AUCTION_BID_OWN or msg == LE_GAME_ERR_NOT_ENOUGH_MONEY or msg == LE_GAME_ERR_ITEM_MAX_COUNT then
-- failed to bid/buy an auction
return "ST_CONFIRMING_BID_BUY", false
elseif context.searchContext:IsBidScan() and msg == ERR_AUCTION_BID_PLACED then
-- bid on an auction
return "ST_CONFIRMING_BID_BUY", true
elseif context.searchContext:IsBuyoutScan() and msg == format(ERR_AUCTION_WON_S, ItemInfo.GetName(rawLink)) then
-- bought an auction
return "ST_CONFIRMING_BID_BUY", true
end
end)
:AddEvent("EV_BUYOUT_SUCCESS", function(context)
return "ST_CONFIRMING_BID_BUY", true
end)
:AddEvent("EV_AUCTION_ID_UPDATE", function(context, oldAuctionId, newAuctionId, newResultInfo)
if not context.findAuction or select(2, context.findAuction:GetListingInfo()) ~= oldAuctionId then
return
end
context.findAuction:UpdateResultInfo(newAuctionId, newResultInfo)
context.findHash = context.findAuction:GetHashes()
end)
)
:AddState(FSM.NewState("ST_BID_BUY_CONFIRMATION")
:SetOnEnter(function(context)
local selection = context.scanFrame:GetElement("auctions"):GetSelection()
local index = TSM.IsWowClassic() and context.findResult[#context.findResult] or nil
if TSM.UI.AuctionUI.BuyUtil.ShowConfirmation(context.scanFrame, selection, context.searchContext:IsBuyoutScan(), context.numConfirmed + 1, context.numFound, context.maxQuantity, private.FSMConfirmationCallback, context.auctionScan, index, true, context.searchContext:GetMarketValueFunc()) then
return "ST_BIDDING_BUYING"
else
local quantity = selection:GetQuantities()
return "ST_PLACING_BID_BUY", quantity
end
end)
:AddTransition("ST_PLACING_BID_BUY")
:AddTransition("ST_BIDDING_BUYING")
)
:AddState(FSM.NewState("ST_PLACING_BID_BUY")
:SetOnEnter(function(context, quantity)
local index = TSM.IsWowClassic() and tremove(context.findResult, #context.findResult) or nil
assert(not TSM.IsWowClassic() or index)
local bidBuyout = nil
if context.searchContext:IsBuyoutScan() then
bidBuyout = context.findAuction:GetBuyouts()
elseif context.searchContext:IsBidScan() then
bidBuyout = context.findAuction:GetRequiredBid()
else
error("Invalid scan type")
end
local result = context.auctionScan:PlaceBidOrBuyout(index, bidBuyout, context.findAuction, quantity)
if result then
MailTracking.RecordAuctionBuyout(ItemString.GetBaseFast(context.findAuction:GetItemString()), quantity)
context.numActioned = context.numActioned + (TSM.IsWowClassic() and 1 or quantity)
context.lastBuyQuantity = quantity
else
local _, rawLink = context.findAuction:GetLinks()
if context.searchContext:IsBuyoutScan() then
Log.PrintfUser(L["Failed to buy auction of %s (x%s) for %s."], rawLink, quantity, Money.ToString(bidBuyout, nil, "OPT_83_NO_COPPER"))
elseif context.searchContext:IsBidScan() then
Log.PrintfUser(L["Failed to bid on auction of %s (x%s) for %s."], rawLink, quantity, Money.ToString(bidBuyout, nil, "OPT_83_NO_COPPER"))
else
error("Invalid scan type")
end
end
return "ST_BIDDING_BUYING"
end)
:AddTransition("ST_BIDDING_BUYING")
)
:AddState(FSM.NewState("ST_CONFIRMING_BID_BUY")
:SetOnEnter(function(context, success)
if not success then
local _, rawLink = context.findAuction:GetLinks()
local quantity = context.findAuction:GetQuantities()
local bidBuyout = nil
if context.searchContext:IsBuyoutScan() then
bidBuyout = context.findAuction:GetBuyouts()
elseif context.searchContext:IsBidScan() then
bidBuyout = context.findAuction:GetRequiredBid()
else
error("Invalid scan type")
end
if context.searchContext:IsBuyoutScan() then
Log.PrintfUser(L["Failed to buy auction of %s (x%s) for %s."], rawLink, quantity, Money.ToString(bidBuyout, nil, "OPT_83_NO_COPPER"))
elseif context.searchContext:IsBidScan() then
Log.PrintfUser(L["Failed to bid on auction of %s (x%s) for %s."], rawLink, quantity, Money.ToString(bidBuyout, nil, "OPT_83_NO_COPPER"))
else
error("Invalid scan type")
end
end
context.numConfirmed = context.numConfirmed + (TSM.IsWowClassic() and 1 or context.lastBuyQuantity)
context.findAuction = context.scanFrame and context.scanFrame:GetElement("auctions"):GetSelection()
return "ST_BIDDING_BUYING", context.lastBuyQuantity
end)
:AddTransition("ST_BIDDING_BUYING")
)
:AddDefaultEvent("EV_SCAN_FRAME_SHOWN", function(context, scanFrame)
context.scanFrame = scanFrame
UpdateScanFrame(context)
end)
:AddDefaultEvent("EV_SCAN_FRAME_HIDDEN", function(context)
context.scanFrame = nil
context.findAuction = nil
end)
:AddDefaultEventTransition("EV_AUCTION_HOUSE_CLOSED", "ST_INIT")
:AddDefaultEventTransition("EV_STOP_CLICKED", "ST_INIT")
:AddDefaultEvent("EV_AUCTION_ROW_REMOVED", function(context, row)
if not row:IsSubRow() then
return
end
row:GetResultRow():RemoveSubRow(row)
context.scanFrame:GetElement("auctions"):UpdateData(true)
end)
:Init("ST_INIT", fsmContext)
end
function private.FSMMessageEventHandler(_, msg)
private.fsm:SetLoggingEnabled(false)
private.fsm:ProcessEvent("EV_MSG", msg)
private.fsm:SetLoggingEnabled(true)
end
function private.FSMBuyoutSuccess()
private.fsm:ProcessEvent("EV_BUYOUT_SUCCESS")
end
function private.FSMRescanDelayed()
private.fsm:ProcessEvent("EV_RESCAN_DELAYED")
end
function private.FSMScanCallback(success)
if success then
private.fsm:ProcessEvent("EV_SCAN_COMPLETE")
else
private.fsm:ProcessEvent("EV_SCAN_FAILED")
end
end
function private.FSMPhasedCallback()
private.fsm:ProcessEvent("EV_PHASED")
end
function private.FSMFindAuctionCallback(result)
if result then
private.fsm:ProcessEvent("EV_AUCTION_FOUND", result)
else
private.fsm:ProcessEvent("EV_AUCTION_NOT_FOUND")
end
end
function private.FSMConfirmationCallback(isBuy, quantity)
private.fsm:ProcessEvent("EV_CONFIRMED", isBuy, quantity)
end

556
Core/UI/BankingUI/Core.lua Normal file
View File

@@ -0,0 +1,556 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local BankingUI = TSM.UI:NewPackage("BankingUI")
local L = TSM.Include("Locale").GetTable()
local FSM = TSM.Include("Util.FSM")
local TempTable = TSM.Include("Util.TempTable")
local Log = TSM.Include("Util.Log")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
local private = {
settings = nil,
fsm = nil,
groupSearch = "",
}
local MIN_FRAME_SIZE = { width = 325, height = 600 }
local MODULE_LIST = {
"Warehousing",
"Auctioning",
"Mailing",
}
local BUTTON_TEXT_LOOKUP = {
Warehousing = L["Warehousing"],
Auctioning = L["Auctioning"],
Mailing = L["Mailing"],
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function BankingUI.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "bankingUIContext", "frame")
:AddKey("global", "bankingUIContext", "isOpen")
:AddKey("global", "bankingUIContext", "tab")
:AddKey("char", "bankingUIContext", "warehousingGroupTree")
:AddKey("char", "bankingUIContext", "auctioningGroupTree")
:AddKey("char", "bankingUIContext", "mailingGroupTree")
private.FSMCreate()
end
function BankingUI.OnDisable()
-- hide the frame
private.fsm:ProcessEvent("EV_BANK_CLOSED")
end
function BankingUI.Toggle()
private.fsm:ProcessEvent("EV_TOGGLE")
end
-- ============================================================================
-- Main Frame
-- ============================================================================
function private.CreateMainFrame()
TSM.UI.AnalyticsRecordPathChange("banking")
local frame = UIElements.New("ApplicationFrame", "base")
:SetParent(UIParent)
:SetSettingsContext(private.settings, "frame")
:SetMinResize(MIN_FRAME_SIZE.width, MIN_FRAME_SIZE.height)
:SetStrata("HIGH")
:SetTitle(L["Banking"])
:SetScript("OnHide", private.BaseFrameOnHide)
:SetContentFrame(UIElements.New("Frame", "content")
:SetLayout("VERTICAL")
:SetBackgroundColor("PRIMARY_BG")
:AddChild(UIElements.New("Frame", "navButtons")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(8)
:SetPadding(-4, 0, 0, 0) -- account for the left margin of the first button
)
:AddChild(UIElements.New("Frame", "search")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(8, 8, 0, 12)
:AddChild(UIElements.New("Input", "input")
:SetIconTexture("iconPack.18x18/Search")
:AllowItemInsert(true)
:SetClearButtonEnabled(true)
:SetValue(private.groupSearch)
:SetHintText(L["Search Groups"])
:SetScript("OnValueChanged", private.GroupSearchOnValueChanged)
)
:AddChild(UIElements.New("Button", "expandAllBtn")
:SetSize(24, 24)
:SetMargin(8, 4, 0, 0)
:SetBackground("iconPack.18x18/Expand All")
:SetScript("OnClick", private.ExpandAllGroupsOnClick)
:SetTooltip(L["Expand / Collapse All Groups"])
)
:AddChild(UIElements.New("Button", "selectAllBtn")
:SetSize(24, 24)
:SetBackground("iconPack.18x18/Select All")
:SetScript("OnClick", private.SelectAllGroupsOnClick)
:SetTooltip(L["Select / Deselect All Groups"])
)
)
:AddChild(UIElements.New("ApplicationGroupTree", "groupTree")
:SetSettingsContext(private.settings, private.GetSettingsContextKey())
:SetQuery(TSM.Groups.CreateQuery(), private.settings.tab)
:SetSearchString(private.groupSearch)
)
:AddChild(UIElements.New("Texture", "line")
:SetHeight(2)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Frame", "footer")
:SetLayout("VERTICAL")
:SetHeight(170)
:SetPadding(8)
:SetBackgroundColor("PRIMARY_BG_ALT")
:AddChild(UIElements.New("ProgressBar", "progressBar")
:SetHeight(24)
:SetProgress(0)
:SetProgressIconHidden(true)
:SetText(L["Select Action"])
)
:AddChild(UIElements.New("Frame", "buttons")
:SetLayout("VERTICAL")
)
)
)
frame:GetElement("titleFrame.closeBtn"):SetScript("OnClick", private.CloseBtnOnClick)
for _, module in ipairs(MODULE_LIST) do
frame:GetElement("content.navButtons"):AddChild(UIElements.New("ActionButton", "navBtn_"..module)
:SetHeight(20)
:SetMargin(4, 0, 0, 0)
:SetFont("BODY_BODY3_MEDIUM")
:SetContext(module)
:SetText(BUTTON_TEXT_LOOKUP[module])
:SetScript("OnClick", private.NavBtnOnClick)
)
end
private.UpdateCurrentModule(frame)
return frame
end
function private.GetSettingsContextKey()
if private.settings.tab == "Warehousing" then
return "warehousingGroupTree"
elseif private.settings.tab == "Auctioning" then
return "auctioningGroupTree"
elseif private.settings.tab == "Mailing" then
return "mailingGroupTree"
else
error("Unexpected tab: "..tostring(private.settings.tab))
end
end
function private.UpdateCurrentModule(frame)
if not TSM.IsWowClassic() then
ReagentBankFrame_OnShow(ReagentBankFrame)
end
-- update nav buttons
local navButtonsFrame = frame:GetElement("content.navButtons")
for _, module in ipairs(MODULE_LIST) do
navButtonsFrame:GetElement("navBtn_"..module)
:SetPressed(module == private.settings.tab)
end
navButtonsFrame:Draw()
-- update group tree
frame:GetElement("content.groupTree")
:SetSettingsContext(private.settings, private.GetSettingsContextKey())
:SetQuery(TSM.Groups.CreateQuery(), private.settings.tab)
:UpdateData(true)
:Draw()
-- update footer buttons
local footerButtonsFrame = frame:GetElement("content.footer.buttons")
footerButtonsFrame:ReleaseAllChildren()
if private.settings.tab == "Warehousing" then
footerButtonsFrame:AddChild(UIElements.New("Frame", "row1")
:SetLayout("HORIZONTAL")
:AddChild(UIElements.New("ActionButton", "moveBankBtn")
:SetHeight(24)
:SetMargin(0, 8, 8, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Move to bank"])
:SetContext(TSM.Banking.Warehousing.MoveGroupsToBank)
:SetScript("OnClick", private.GroupBtnOnClick)
)
:AddChild(UIElements.New("ActionButton", "moveBagsBtn")
:SetHeight(24)
:SetMargin(0, 0, 8, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Move to bags"])
:SetContext(TSM.Banking.Warehousing.MoveGroupsToBags)
:SetScript("OnClick", private.GroupBtnOnClick)
)
)
footerButtonsFrame:AddChild(UIElements.New("ActionButton", "restockBagsBtn")
:SetHeight(24)
:SetMargin(0, 0, 8, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Restock bags"])
:SetContext(TSM.Banking.Warehousing.RestockBags)
:SetScript("OnClick", private.GroupBtnOnClick)
)
footerButtonsFrame:AddChild(UIElements.New("ActionButton", "depositReagentsBtn")
:SetHeight(24)
:SetMargin(0, 0, 8, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetDisabled(TSM.IsWowClassic())
:SetText(L["Deposit reagents"])
:SetScript("OnClick", private.WarehousingDepositReagentsBtnOnClick)
)
footerButtonsFrame:AddChild(UIElements.New("Frame", "row4")
:SetLayout("HORIZONTAL")
:AddChild(UIElements.New("ActionButton", "emptyBagsBtn")
:SetHeight(24)
:SetMargin(0, 8, 8, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Empty bags"])
:SetContext(TSM.Banking.EmptyBags)
:SetScript("OnClick", private.SimpleBtnOnClick)
)
:AddChild(UIElements.New("ActionButton", "restoreBagsBtn")
:SetHeight(24)
:SetMargin(0, 0, 8, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Restore bags"])
:SetContext(TSM.Banking.RestoreBags)
:SetScript("OnClick", private.SimpleBtnOnClick)
)
)
elseif private.settings.tab == "Auctioning" then
footerButtonsFrame:AddChild(UIElements.New("ActionButton", "moveBankBtn")
:SetHeight(24)
:SetMargin(0, 0, 8, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Move to bank"])
:SetContext(TSM.Banking.Auctioning.MoveGroupsToBank)
:SetScript("OnClick", private.GroupBtnOnClick)
)
footerButtonsFrame:AddChild(UIElements.New("ActionButton", "postCapBagsBtn")
:SetHeight(24)
:SetMargin(0, 0, 8, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Post cap to bags"])
:SetContext(TSM.Banking.Auctioning.PostCapToBags)
:SetScript("OnClick", private.GroupBtnOnClick)
)
footerButtonsFrame:AddChild(UIElements.New("ActionButton", "shortfallBagsBtn")
:SetHeight(24)
:SetMargin(0, 0, 8, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Shortfall to bags"])
:SetContext(TSM.Banking.Auctioning.ShortfallToBags)
:SetScript("OnClick", private.GroupBtnOnClick)
)
footerButtonsFrame:AddChild(UIElements.New("ActionButton", "maxExpBankBtn")
:SetHeight(24)
:SetMargin(0, 0, 8, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Max expires to bank"])
:SetContext(TSM.Banking.Auctioning.MaxExpiresToBank)
:SetScript("OnClick", private.GroupBtnOnClick)
)
elseif private.settings.tab == "Mailing" then
footerButtonsFrame:AddChild(UIElements.New("ActionButton", "moveBankBtn")
:SetHeight(24)
:SetMargin(0, 0, 8, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Move to bank"])
:SetContext(TSM.Banking.Mailing.MoveGroupsToBank)
:SetScript("OnClick", private.GroupBtnOnClick)
)
footerButtonsFrame:AddChild(UIElements.New("ActionButton", "nongroupBankBtn")
:SetHeight(24)
:SetMargin(0, 0, 8, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Nongroup to bank"])
:SetContext(TSM.Banking.Mailing.NongroupToBank)
:SetScript("OnClick", private.SimpleBtnOnClick)
)
footerButtonsFrame:AddChild(UIElements.New("ActionButton", "targetShortfallBagsBtn")
:SetHeight(24)
:SetMargin(0, 0, 8, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Target shortfall to bags"])
:SetContext(TSM.Banking.Mailing.TargetShortfallToBags)
:SetScript("OnClick", private.GroupBtnOnClick)
)
footerButtonsFrame:AddChild(UIElements.New("Frame", "row4")
:SetLayout("HORIZONTAL")
:AddChild(UIElements.New("ActionButton", "emptyBagsBtn")
:SetHeight(24)
:SetMargin(0, 8, 8, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Empty bags"])
:SetContext(TSM.Banking.EmptyBags)
:SetScript("OnClick", private.SimpleBtnOnClick)
)
:AddChild(UIElements.New("ActionButton", "restoreBagsBtn")
:SetHeight(24)
:SetMargin(0, 0, 8, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Restore bags"])
:SetContext(TSM.Banking.RestoreBags)
:SetScript("OnClick", private.SimpleBtnOnClick)
)
)
else
error("Unexpected module: "..tostring(private.settings.tab))
end
footerButtonsFrame:Draw()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.BaseFrameOnHide()
TSM.UI.AnalyticsRecordClose("banking")
end
function private.CloseBtnOnClick(button)
Log.PrintUser(L["Hiding the TSM Banking UI. Type '/tsm bankui' to reopen it."])
button:GetParentElement():Hide()
private.fsm:ProcessEvent("EV_FRAME_HIDDEN")
end
function private.GroupSearchOnValueChanged(input)
private.groupSearch = strlower(input:GetValue())
input:GetElement("__parent.__parent.groupTree")
:SetSearchString(private.groupSearch)
:Draw()
end
function private.NavBtnOnClick(button)
private.settings.tab = button:GetContext()
private.UpdateCurrentModule(button:GetBaseElement())
private.fsm:ProcessEvent("EV_NAV_CHANGED")
end
function private.ExpandAllGroupsOnClick(button)
button:GetElement("__parent.__parent.groupTree")
:ToggleExpandAll()
end
function private.SelectAllGroupsOnClick(button)
button:GetElement("__parent.__parent.groupTree")
:ToggleSelectAll()
end
function private.WarehousingDepositReagentsBtnOnClick()
DepositReagentBank()
end
function private.SimpleBtnOnClick(button)
private.fsm:ProcessEvent("EV_BUTTON_CLICKED", button, button:GetContext())
end
function private.GroupBtnOnClick(button)
local groups = TempTable.Acquire()
for _, groupPath in button:GetElement("__base.content.groupTree"):SelectedGroupsIterator() do
groups[groupPath] = true
end
private.fsm:ProcessEvent("EV_BUTTON_CLICKED", button, button:GetContext(), groups)
TempTable.Release(groups)
end
-- ============================================================================
-- FSM
-- ============================================================================
function private.FSMCreate()
TSM.Banking.RegisterFrameCallback(function(openFrame)
private.fsm:ProcessEvent(openFrame and "EV_BANK_OPENED" or "EV_BANK_CLOSED")
end)
local fsmContext = {
frame = nil,
progress = nil,
activeButton = nil,
}
local function UpdateFrame(context)
if context.activeButton and not context.progress then
context.activeButton
:SetPressed(false)
:Draw()
context.activeButton = nil
end
-- update the nav button state
local navButtonsFrame = context.frame:GetElement("content.navButtons")
for _, module in ipairs(MODULE_LIST) do
navButtonsFrame:GetElement("navBtn_"..module)
:SetDisabled(context.progress)
end
navButtonsFrame:Draw()
-- update the progress bar
context.frame:GetElement("content.footer.progressBar")
:SetProgress(context.progress or 0)
:SetProgressIconHidden(not context.progress)
:SetText(context.progress and L["Moving"] or L["Select Action"])
:Draw()
-- update the action button state
local footerButtonsFrame = context.frame:GetElement("content.footer.buttons")
if private.settings.tab == "Warehousing" then
footerButtonsFrame:GetElement("row1.moveBankBtn")
:SetDisabled(context.progress)
footerButtonsFrame:GetElement("row1.moveBagsBtn")
:SetDisabled(context.progress)
footerButtonsFrame:GetElement("restockBagsBtn")
:SetDisabled(context.progress)
footerButtonsFrame:GetElement("depositReagentsBtn")
:SetDisabled(context.progress or TSM.IsWowClassic())
footerButtonsFrame:GetElement("row4.emptyBagsBtn")
:SetDisabled(context.progress)
footerButtonsFrame:GetElement("row4.restoreBagsBtn")
:SetDisabled(context.progress or not TSM.Banking.CanRestoreBags())
elseif private.settings.tab == "Auctioning" then
footerButtonsFrame:GetElement("moveBankBtn")
:SetDisabled(context.progress)
footerButtonsFrame:GetElement("postCapBagsBtn")
:SetDisabled(context.progress)
footerButtonsFrame:GetElement("shortfallBagsBtn")
:SetDisabled(context.progress)
footerButtonsFrame:GetElement("maxExpBankBtn")
:SetDisabled(context.progress)
elseif private.settings.tab == "Mailing" then
footerButtonsFrame:GetElement("moveBankBtn")
:SetDisabled(context.progress)
footerButtonsFrame:GetElement("nongroupBankBtn")
:SetDisabled(context.progress)
footerButtonsFrame:GetElement("targetShortfallBagsBtn")
:SetDisabled(context.progress)
footerButtonsFrame:GetElement("row4.emptyBagsBtn")
:SetDisabled(context.progress)
footerButtonsFrame:GetElement("row4.restoreBagsBtn")
:SetDisabled(context.progress or not TSM.Banking.CanRestoreBags())
else
error("Unexpected module: "..tostring(private.settings.tab))
end
footerButtonsFrame:Draw()
end
private.fsm = FSM.New("BANKING_UI")
:AddState(FSM.NewState("ST_CLOSED")
:SetOnEnter(function(context)
if context.frame then
context.frame:Hide()
context.frame:Release()
context.frame = nil
end
context.activeButton = nil
end)
:AddTransition("ST_CLOSED")
:AddTransition("ST_FRAME_OPEN")
:AddTransition("ST_FRAME_HIDDEN")
:AddEvent("EV_BANK_OPENED", function(context)
assert(not context.frame)
if not private.settings.isOpen then
return "ST_FRAME_HIDDEN"
end
return "ST_FRAME_OPEN"
end)
)
:AddState(FSM.NewState("ST_FRAME_HIDDEN")
:SetOnEnter(function(context)
private.settings.isOpen = false
if context.frame then
context.frame:Hide()
context.frame:Release()
context.frame = nil
end
context.activeButton = nil
end)
:AddTransition("ST_FRAME_OPEN")
:AddTransition("ST_CLOSED")
:AddEvent("EV_TOGGLE", function()
private.settings.isOpen = true
return "ST_FRAME_OPEN"
end)
)
:AddState(FSM.NewState("ST_FRAME_OPEN")
:SetOnEnter(function(context)
if not context.frame then
context.frame = private.CreateMainFrame()
context.frame:Show()
context.frame:Draw()
end
UpdateFrame(context)
end)
:AddTransition("ST_FRAME_OPEN")
:AddTransition("ST_FRAME_HIDDEN")
:AddTransition("ST_PROCESSING")
:AddTransition("ST_CLOSED")
:AddEventTransition("EV_BUTTON_CLICKED", "ST_PROCESSING")
:AddEventTransition("EV_TOGGLE", "ST_FRAME_HIDDEN")
:AddEventTransition("EV_FRAME_HIDDEN", "ST_FRAME_HIDDEN")
:AddEventTransition("EV_NAV_CHANGED", "ST_FRAME_OPEN")
)
:AddState(FSM.NewState("ST_PROCESSING")
:SetOnEnter(function(context, button, startFunc, ...)
context.activeButton = button
context.activeButton
:SetPressed(true)
:Draw()
context.progress = 0
startFunc(private.FSMThreadCallback, ...)
UpdateFrame(context)
end)
:SetOnExit(function(context)
context.progress = nil
end)
:AddTransition("ST_FRAME_OPEN")
:AddTransition("ST_FRAME_HIDDEN")
:AddTransition("ST_CLOSED")
:AddEvent("EV_THREAD_PROGRESS", function(context, progress)
context.progress = progress
UpdateFrame(context)
end)
:AddEvent("EV_THREAD_DONE", function(context)
if context.progress == 0 then
Log.PrintUser(L["Nothing to move."])
end
return "ST_FRAME_OPEN"
end)
:AddEventTransition("EV_TOGGLE", "ST_FRAME_HIDDEN")
)
:AddDefaultEventTransition("EV_BANK_CLOSED", "ST_CLOSED")
:Init("ST_CLOSED", fsmContext)
end
function private.FSMThreadCallback(event, ...)
if event == "PROGRESS" then
private.fsm:ProcessEvent("EV_THREAD_PROGRESS", ...)
elseif event == "DONE" then
private.fsm:ProcessEvent("EV_THREAD_DONE")
elseif event == "MOVED" then
-- ignore this event
else
error("Unexpected event: "..tostring(event))
end
end

340
Core/UI/CraftingUI/Core.lua Normal file
View File

@@ -0,0 +1,340 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local CraftingUI = TSM.UI:NewPackage("CraftingUI")
local L = TSM.Include("Locale").GetTable()
local FSM = TSM.Include("Util.FSM")
local Event = TSM.Include("Util.Event")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
local private = {
settings = nil,
topLevelPages = {},
fsm = nil,
craftOpen = nil,
tradeSkillOpen = nil,
defaultUISwitchBtn = nil,
isVisible = false,
}
local MIN_FRAME_SIZE = { width = 650, height = 587 }
local BEAST_TRAINING_DE = "Bestienausbildung"
local BEAST_TRAINING_ES = "Entrenamiento de bestias"
local BEAST_TRAINING_RUS = "Воспитание питомца"
local IGNORED_PROFESSIONS = {
[53428] = true, -- Runeforging
[158756] = true, -- Skinning Skills
[193290] = true, -- Herbalism Skills
[7620] = true, -- Fishing Skills (shows up as Fishing)
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function CraftingUI.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "craftingUIContext", "showDefault")
:AddKey("global", "craftingUIContext", "frame")
private.FSMCreate()
TSM.Crafting.ProfessionScanner.SetDisabled(private.settings.showDefault)
end
function CraftingUI.OnDisable()
-- hide the frame
if private.isVisible then
TSM.Crafting.ProfessionScanner.SetDisabled(false)
private.fsm:ProcessEvent("EV_FRAME_TOGGLE")
end
end
function CraftingUI.RegisterTopLevelPage(name, callback)
tinsert(private.topLevelPages, { name = name, callback = callback })
end
function CraftingUI.Toggle()
private.settings.showDefault = false
TSM.Crafting.ProfessionScanner.SetDisabled(false)
private.fsm:ProcessEvent("EV_FRAME_TOGGLE")
end
function CraftingUI.IsProfessionIgnored(name)
if TSM.IsWowClassic() then
if name == GetSpellInfo(5149) or name == BEAST_TRAINING_DE or name == BEAST_TRAINING_ES or name == BEAST_TRAINING_RUS then -- Beast Training
return true
elseif name == GetSpellInfo(7620) then -- Fishing
return true
elseif name == GetSpellInfo(2366) then -- Herb Gathering
return true
elseif name == GetSpellInfo(8613) then -- Skinning
return true
end
end
for i in pairs(IGNORED_PROFESSIONS) do
local ignoredName = GetSpellInfo(i)
if ignoredName == name then
return true
end
end
end
function CraftingUI.IsVisible()
return private.isVisible
end
-- ============================================================================
-- Main Frame
-- ============================================================================
function private.CreateMainFrame()
TSM.UI.AnalyticsRecordPathChange("crafting")
local frame = UIElements.New("LargeApplicationFrame", "base")
:SetParent(UIParent)
:SetSettingsContext(private.settings, "frame")
:SetMinResize(MIN_FRAME_SIZE.width, MIN_FRAME_SIZE.height)
:SetStrata("HIGH")
:AddPlayerGold()
:AddAppStatusIcon()
:AddSwitchButton(private.SwitchBtnOnClick)
:SetScript("OnHide", private.BaseFrameOnHide)
for _, info in ipairs(private.topLevelPages) do
frame:AddNavButton(info.name, info.callback)
end
return frame
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.BaseFrameOnHide()
TSM.UI.AnalyticsRecordClose("crafting")
private.fsm:ProcessEvent("EV_FRAME_HIDE")
end
function private.SwitchBtnOnClick(button)
private.settings.showDefault = button ~= private.defaultUISwitchBtn
TSM.Crafting.ProfessionScanner.SetDisabled(private.settings.showDefault)
private.fsm:ProcessEvent("EV_SWITCH_BTN_CLICKED")
end
function private.SwitchButtonOnEnter(button)
button:SetTextColor("TEXT")
:Draw()
end
function private.SwitchButtonOnLeave(button)
button:SetTextColor("TEXT_ALT")
:Draw()
end
-- ============================================================================
-- FSM
-- ============================================================================
function private.FSMCreate()
if TSM.IsWowClassic() then
Event.Register("CRAFT_SHOW", function()
CloseTradeSkill()
private.craftOpen = true
TSM.Crafting.ProfessionState.SetCraftOpen(true)
private.fsm:ProcessEvent("EV_TRADE_SKILL_SHOW")
end)
Event.Register("CRAFT_CLOSE", function()
private.craftOpen = false
TSM.Crafting.ProfessionState.SetCraftOpen(false)
if not private.tradeSkillOpen then
private.fsm:ProcessEvent("EV_TRADE_SKILL_CLOSED")
end
end)
end
Event.Register("TRADE_SKILL_SHOW", function()
if TSM.IsWowClassic() then
CloseCraft()
end
private.tradeSkillOpen = true
private.fsm:ProcessEvent("EV_TRADE_SKILL_SHOW")
end)
Event.Register("TRADE_SKILL_CLOSE", function()
private.tradeSkillOpen = false
if not private.craftOpen then
private.fsm:ProcessEvent("EV_TRADE_SKILL_CLOSED")
end
end)
-- we'll implement UIParent's event handler directly when necessary for TRADE_SKILL_SHOW
if TSM.IsWowClassic() then
UIParent:UnregisterEvent("CRAFT_SHOW")
end
UIParent:UnregisterEvent("TRADE_SKILL_SHOW")
local fsmContext = {
frame = nil,
}
local function UpdateDefaultCraftButton()
if CraftFrame and CraftCreateButton and private.craftOpen then
CraftCreateButton:SetParent(CraftFrame)
CraftCreateButton:ClearAllPoints()
CraftCreateButton:SetPoint("CENTER", CraftFrame, "TOPLEFT", 224, -422)
CraftCreateButton:SetFrameLevel(2)
CraftCreateButton:EnableDrawLayer("BACKGROUND")
CraftCreateButton:EnableDrawLayer("ARTWORK")
CraftCreateButton:SetHighlightTexture("Interface\\Buttons\\UI-Panel-Button-Highlight")
CraftCreateButton:GetHighlightTexture():SetTexCoord(0, 0.625, 0, 0.6875)
end
end
local function DefaultFrameOnHide()
private.fsm:ProcessEvent("EV_FRAME_HIDE")
end
private.fsm = FSM.New("CRAFTING_UI")
:AddState(FSM.NewState("ST_CLOSED")
:AddTransition("ST_DEFAULT_OPEN")
:AddTransition("ST_FRAME_OPEN")
:AddEvent("EV_FRAME_TOGGLE", function(context)
assert(not private.settings.showDefault)
TSM.Crafting.ProfessionScanner.SetDisabled(false)
return "ST_FRAME_OPEN"
end)
:AddEvent("EV_TRADE_SKILL_SHOW", function(context)
TSM.Crafting.ProfessionScanner.SetDisabled(private.settings.showDefault)
local name = TSM.Crafting.ProfessionUtil.GetCurrentProfessionName()
if CraftingUI.IsProfessionIgnored(name) then
return "ST_DEFAULT_OPEN", true
elseif private.settings.showDefault then
return "ST_DEFAULT_OPEN"
else
return "ST_FRAME_OPEN"
end
end)
)
:AddState(FSM.NewState("ST_DEFAULT_OPEN")
:SetOnEnter(function(context, isIgnored)
if private.craftOpen then
UIParent_OnEvent(UIParent, "CRAFT_SHOW")
UpdateDefaultCraftButton()
else
UIParent_OnEvent(UIParent, "TRADE_SKILL_SHOW")
end
if not private.defaultUISwitchBtn then
private.defaultUISwitchBtn = UIElements.New("ActionButton", "switchBtn")
:SetSize(60, TSM.IsWowClassic() and 16 or 15)
:AddAnchor("TOPRIGHT", TSM.IsWowClassic() and -60 or -27, TSM.IsWowClassic() and -16 or -4)
:SetRelativeLevel(3)
:DisableClickCooldown()
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["TSM4"])
:SetScript("OnClick", private.SwitchBtnOnClick)
:SetScript("OnEnter", private.SwitchButtonOnEnter)
:SetScript("OnLeave", private.SwitchButtonOnLeave)
private.defaultUISwitchBtn:_GetBaseFrame():SetParent(TradeSkillFrame)
end
private.defaultUISwitchBtn:_GetBaseFrame():SetParent(private.craftOpen and CraftFrame or TradeSkillFrame)
if isIgnored then
TSM.Crafting.ProfessionScanner.SetDisabled(true)
private.defaultUISwitchBtn:Hide()
else
private.defaultUISwitchBtn:Show()
private.defaultUISwitchBtn:Draw()
end
if private.craftOpen then
ScriptWrapper.Set(CraftFrame, "OnHide", DefaultFrameOnHide)
else
ScriptWrapper.Set(TradeSkillFrame, "OnHide", DefaultFrameOnHide)
end
if not TSM.IsWowClassic() then
local linked, linkedName = TSM.Crafting.ProfessionUtil.IsLinkedProfession()
if TSM.Crafting.ProfessionUtil.IsDataStable() and not TSM.Crafting.ProfessionUtil.IsGuildProfession() and (not linked or (linked and linkedName == UnitName("player"))) then
TradeSkillFrame:OnEvent("TRADE_SKILL_DATA_SOURCE_CHANGED")
TradeSkillFrame:OnEvent("TRADE_SKILL_LIST_UPDATE")
end
end
end)
:SetOnExit(function(context)
if private.craftOpen then
if CraftFrame then
ScriptWrapper.Clear(CraftFrame, "OnHide")
HideUIPanel(CraftFrame)
end
else
if TradeSkillFrame then
ScriptWrapper.Clear(TradeSkillFrame, "OnHide")
HideUIPanel(TradeSkillFrame)
end
end
end)
:AddTransition("ST_CLOSED")
:AddTransition("ST_FRAME_OPEN")
:AddTransition("ST_DEFAULT_OPEN")
:AddEvent("EV_FRAME_HIDE", function(context)
TSM.Crafting.ProfessionUtil.CloseTradeSkill(false, private.craftOpen)
return "ST_CLOSED"
end)
:AddEvent("EV_TRADE_SKILL_SHOW", function(context)
if CraftingUI.IsProfessionIgnored(TSM.Crafting.ProfessionUtil.GetCurrentProfessionName()) then
return "ST_DEFAULT_OPEN", true
else
if private.settings.showDefault then
return "ST_DEFAULT_OPEN"
else
TSM.Crafting.ProfessionScanner.SetDisabled(private.settings.showDefault)
return "ST_FRAME_OPEN"
end
end
end)
:AddEventTransition("EV_TRADE_SKILL_CLOSED", "ST_CLOSED")
:AddEventTransition("EV_SWITCH_BTN_CLICKED", "ST_FRAME_OPEN")
)
:AddState(FSM.NewState("ST_FRAME_OPEN")
:SetOnEnter(function(context)
assert(not context.frame)
context.frame = private.CreateMainFrame()
context.frame:Show()
if TSM.Crafting.ProfessionUtil.GetCurrentProfessionName() then
context.frame:GetElement("titleFrame.switchBtn"):Show()
else
context.frame:GetElement("titleFrame.switchBtn"):Hide()
end
context.frame:Draw()
private.isVisible = true
end)
:SetOnExit(function(context)
context.frame:Hide()
context.frame:Release()
context.frame = nil
private.isVisible = false
if TSM.IsWowClassic() then
UpdateDefaultCraftButton()
end
end)
:AddTransition("ST_CLOSED")
:AddTransition("ST_DEFAULT_OPEN")
:AddEvent("EV_FRAME_HIDE", function(context)
TSM.Crafting.ProfessionUtil.CloseTradeSkill(true)
return "ST_CLOSED"
end)
:AddEvent("EV_TRADE_SKILL_SHOW", function(context)
if CraftingUI.IsProfessionIgnored(TSM.Crafting.ProfessionUtil.GetCurrentProfessionName()) then
return "ST_DEFAULT_OPEN", true
end
context.frame:GetElement("titleFrame.switchBtn"):Show()
context.frame:GetElement("titleFrame"):Draw()
end)
:AddEventTransition("EV_TRADE_SKILL_CLOSED", "ST_CLOSED")
:AddEventTransition("EV_SWITCH_BTN_CLICKED", "ST_DEFAULT_OPEN")
:AddEventTransition("EV_FRAME_TOGGLE", "ST_CLOSED")
)
:Init("ST_CLOSED", fsmContext)
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,673 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local CraftingReports = TSM.UI.CraftingUI:NewPackage("CraftingReports")
local L = TSM.Include("Locale").GetTable()
local Math = TSM.Include("Util.Math")
local Log = TSM.Include("Util.Log")
local Money = TSM.Include("Util.Money")
local String = TSM.Include("Util.String")
local ItemString = TSM.Include("Util.ItemString")
local Theme = TSM.Include("Util.Theme")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
local private = {
settings = nil,
craftsQuery = nil,
matsQuery = nil,
filterText = "",
craftProfessions = {},
matProfessions = {},
}
local MAT_PRICE_SOURCES = {ALL, L["Default Price"], L["Custom Price"]}
-- ============================================================================
-- Module Functions
-- ============================================================================
function CraftingReports.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "craftingUIContext", "craftsScrollingTable")
:AddKey("global", "craftingUIContext", "matsScrollingTable")
:AddKey("factionrealm", "internalData", "mats")
:AddKey("global", "craftingOptions", "defaultMatCostMethod")
TSM.UI.CraftingUI.RegisterTopLevelPage(L["Reports"], private.GetCraftingReportsFrame)
end
-- ============================================================================
-- CraftingReports UI
-- ============================================================================
function private.GetCraftingReportsFrame()
TSM.UI.AnalyticsRecordPathChange("crafting", "crafting_reports")
if not private.craftsQuery then
private.craftsQuery = TSM.Crafting.CreateCraftsQuery()
private.craftsQuery:VirtualField("firstOperation", "string", private.FirstOperationVirtualField, "itemString")
end
private.craftsQuery:ResetFilters()
private.craftsQuery:ResetOrderBy()
private.craftsQuery:OrderBy("itemName", true)
private.matsQuery = private.matsQuery or TSM.Crafting.CreateMatItemQuery()
private.matsQuery:ResetFilters()
private.matsQuery:ResetOrderBy()
private.matsQuery:OrderBy("name", true)
return UIElements.New("Frame", "craftingReportsContent")
:SetLayout("VERTICAL")
:SetPadding(0, 0, 6, 0)
:SetBackgroundColor("PRIMARY_BG_ALT")
:AddChild(UIElements.New("TabGroup", "buttons")
:SetNavCallback(private.GetTabElements)
:AddPath(L["Crafts"], true)
:AddPath(L["Materials"])
)
end
function private.GetTabElements(self, path)
if path == L["Crafts"] then
TSM.UI.AnalyticsRecordPathChange("crafting", "crafting_reports", "crafts")
private.filterText = ""
wipe(private.craftProfessions)
tinsert(private.craftProfessions, L["All Professions"])
for _, player, profession in TSM.Crafting.PlayerProfessions.Iterator() do
tinsert(private.craftProfessions, format("%s - %s", profession, player))
end
private.craftsQuery:ResetFilters()
return UIElements.New("Frame", "crafts")
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Frame", "filters")
:SetLayout("HORIZONTAL")
:SetHeight(72)
:SetPadding(10, 10, 8, 16)
:AddChild(UIElements.New("Frame", "search")
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Text", "label")
:SetHeight(20)
:SetMargin(0, 0, 0, 4)
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["Filter by Keyword"])
)
:AddChild(UIElements.New("Input", "input")
:SetHeight(24)
:AllowItemInsert()
:SetIconTexture("iconPack.18x18/Search")
:SetClearButtonEnabled(true)
:SetHintText(L["Enter Keyword"])
:SetScript("OnValueChanged", private.CraftsInputOnValueChanged)
)
)
:AddChild(UIElements.New("Frame", "profession")
:SetLayout("VERTICAL")
:SetMargin(16, 16, 0, 0)
:AddChild(UIElements.New("Text", "label")
:SetHeight(20)
:SetMargin(0, 0, 0, 4)
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["Filter by Profession"])
)
:AddChild(UIElements.New("SelectionDropdown", "dropdown")
:SetHeight(24)
:SetItems(private.craftProfessions)
:SetSelectedItem(private.craftProfessions[1], true)
:SetScript("OnSelectionChanged", private.CraftsDropdownOnSelectionChanged)
)
)
:AddChild(UIElements.New("Frame", "craftable")
:SetLayout("HORIZONTAL")
:SetSize(176, 24)
:SetMargin(0, 0, 24, 0)
:AddChild(UIElements.New("Checkbox", "checkbox")
:SetWidth(24)
:SetFont("BODY_BODY2")
:SetScript("OnValueChanged", private.CheckboxOnValueChanged)
)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY2")
:SetText(L["Only show craftable"])
)
)
)
:AddChild(UIElements.New("QueryScrollingTable", "crafts")
:SetSettingsContext(private.settings, "craftsScrollingTable")
:GetScrollingTableInfo()
:SetMenuInfo(private.CraftsMatsMenuIterator, private.CraftsMenuClickHandler)
:NewColumn("queued")
:SetTitleIcon("iconPack.18x18/Queue")
:SetFont("TABLE_TABLE1")
:SetJustifyH("CENTER")
:SetTextInfo("num")
:SetSortInfo("num")
:Commit()
:NewColumn("craftName")
:SetTitle(L["Name"])
:SetIconSize(12)
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo(nil, private.CraftsGetCraftNameText)
:SetIconInfo("itemString", ItemInfo.GetTexture)
:SetTooltipInfo("itemString")
:SetSortInfo("itemName")
:DisableHiding()
:Commit()
:NewColumn("operation")
:SetTitle(L["Operation"])
:SetFont("BODY_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("firstOperation")
:SetSortInfo("firstOperation")
:Commit()
:NewColumn("bags")
:SetTitle(L["Bag"])
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextInfo("bagQuantity", private.CraftsGetBagsText)
:SetSortInfo("bagQuantity")
:Commit()
:NewColumn("ah")
:SetTitle(L["AH"])
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextInfo("auctionQuantity", private.CraftsGetAHText)
:SetSortInfo("auctionQuantity")
:Commit()
:NewColumn("craftingCost")
:SetTitle(L["Crafting Cost"])
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextInfo("craftingCost", private.CraftsGetCostItemValueText)
:SetSortInfo("craftingCost")
:Commit()
:NewColumn("itemValue")
:SetTitle(L["Item Value"])
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextInfo("itemValue", private.CraftsGetCostItemValueText)
:SetSortInfo("itemValue")
:Commit()
:NewColumn("profit")
:SetTitle(L["Profit"])
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextInfo("profit", private.CraftsGetProfitText)
:SetSortInfo("profit")
:Commit()
:NewColumn("profitPct")
:SetTitle("%")
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextInfo("profitPct", private.GetProfitPctText)
:SetSortInfo("profitPct")
:Commit()
:NewColumn("saleRate")
:SetTitleIcon("iconPack.18x18/SaleRate")
:SetFont("TABLE_TABLE1")
:SetJustifyH("CENTER")
:SetTextInfo("saleRate", private.CraftsGetSaleRateText)
:SetSortInfo("saleRate")
:Commit()
:Commit()
:SetQuery(private.craftsQuery)
:SetSelectionDisabled(true)
:SetScript("OnRowClick", private.CraftsOnRowClick)
)
elseif path == L["Materials"] then
TSM.UI.AnalyticsRecordPathChange("crafting", "crafting_reports", "materials")
wipe(private.matProfessions)
tinsert(private.matProfessions, L["All Professions"])
for _, _, profession in TSM.Crafting.PlayerProfessions.Iterator() do
if not private.matProfessions[profession] then
tinsert(private.matProfessions, profession)
private.matProfessions[profession] = true
end
end
private.matsQuery:ResetFilters()
return UIElements.New("Frame", "materials")
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Frame", "filters")
:SetLayout("HORIZONTAL")
:SetHeight(72)
:SetPadding(10, 10, 8, 16)
:AddChild(UIElements.New("Frame", "search")
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Text", "label")
:SetHeight(20)
:SetMargin(0, 0, 0, 4)
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["Filter by Keyword"])
)
:AddChild(UIElements.New("Input", "input")
:SetHeight(24)
:AllowItemInsert()
:SetIconTexture("iconPack.18x18/Search")
:SetClearButtonEnabled(true)
:SetHintText(L["Enter Keyword"])
:SetScript("OnValueChanged", private.MatsInputOnValueChanged)
)
)
:AddChild(UIElements.New("Frame", "profession")
:SetLayout("VERTICAL")
:SetMargin(16, 0, 0, 0)
:AddChild(UIElements.New("Text", "label")
:SetHeight(20)
:SetMargin(0, 0, 0, 4)
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["Filter by Profession"])
)
:AddChild(UIElements.New("SelectionDropdown", "dropdown")
:SetHeight(24)
:SetItems(private.matProfessions)
:SetSelectedItem(private.matProfessions[1], true)
:SetScript("OnSelectionChanged", private.MatsDropdownOnSelectionChanged)
)
)
:AddChild(UIElements.New("Frame", "priceSource")
:SetLayout("VERTICAL")
:SetMargin(16, 0, 0, 0)
:AddChild(UIElements.New("Text", "label")
:SetHeight(20)
:SetMargin(0, 0, 0, 4)
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["Filter by Price Source"])
)
:AddChild(UIElements.New("SelectionDropdown", "dropdown")
:SetHeight(24)
:SetItems(MAT_PRICE_SOURCES)
:SetSelectedItem(MAT_PRICE_SOURCES[1], true)
:SetScript("OnSelectionChanged", private.MatsDropdownOnSelectionChanged)
)
)
)
:AddChild(UIElements.New("QueryScrollingTable", "mats")
:SetSettingsContext(private.settings, "matsScrollingTable")
:GetScrollingTableInfo()
:SetMenuInfo(private.CraftsMatsMenuIterator, private.MatsMenuClickHandler)
:NewColumn("name")
:SetTitle(L["Name"])
:SetIconSize(12)
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("itemString", private.MatsGetNameText)
:SetIconInfo("itemString", ItemInfo.GetTexture)
:SetTooltipInfo("itemString")
:SetSortInfo("name")
:DisableHiding()
:Commit()
:NewColumn("price")
:SetTitle(L["Mat Price"])
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextInfo("matCost", private.MatsGetPriceText)
:SetSortInfo("matCost")
:Commit()
:NewColumn("professions")
:SetTitle(L["Professions Used In"])
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("professions")
:SetSortInfo("professions")
:Commit()
:NewColumn("num")
:SetTitle(L["Number Owned"])
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextInfo("totalQuantity", private.MatsGetNumText)
:SetSortInfo("totalQuantity")
:Commit()
:Commit()
:SetQuery(private.matsQuery)
:SetSelectionDisabled(true)
:SetScript("OnRowClick", private.MatsOnRowClick)
)
else
error("Unknown path: "..tostring(path))
end
end
-- ============================================================================
-- ScrollingTable Functions
-- ============================================================================
function private.CraftsGetCraftNameText(row)
return TSM.UI.GetColoredItemName(row:GetField("itemString")) or row:GetField("name")
end
function private.CraftsGetBagsText(bagQuantity)
return bagQuantity or "0"
end
function private.CraftsGetAHText(bagQuantity)
return bagQuantity or "0"
end
function private.CraftsGetCostItemValueText(costItemValue)
if Math.IsNan(costItemValue) then
return ""
end
return Money.ToString(costItemValue)
end
function private.CraftsGetProfitText(profit)
if Math.IsNan(profit) then
return ""
end
return Money.ToString(profit, (profit >= 0 and Theme.GetFeedbackColor("GREEN") or Theme.GetFeedbackColor("RED")):GetTextColorPrefix())
end
function private.GetProfitPctText(profitPct)
if Math.IsNan(profitPct) then
return ""
end
local color = Theme.GetFeedbackColor(profitPct >= 0 and "GREEN" or "RED")
return color:ColorText(profitPct.."%") or ""
end
function private.CraftsGetSaleRateText(saleRate)
if Math.IsNan(saleRate) then
return ""
end
return format("%0.2f", saleRate)
end
function private.MatsGetNameText(itemString)
return TSM.UI.GetColoredItemName(itemString) or TSM.UI.GetColoredItemName(ItemString.GetUnknown())
end
function private.MatsGetPriceText(matCost)
if Math.IsNan(matCost) then
return ""
end
return Money.ToString(matCost)
end
function private.MatsGetNumText(totalQuantity)
return totalQuantity or "0"
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.CraftsInputOnValueChanged(input)
local text = input:GetValue()
if text == private.filterText then
return
end
private.filterText = text
private.UpdateCraftsQueryWithFilters(input:GetParentElement():GetParentElement())
end
function private.CraftsDropdownOnSelectionChanged(dropdown)
private.UpdateCraftsQueryWithFilters(dropdown:GetParentElement():GetParentElement())
end
function private.CheckboxOnValueChanged(checkbox)
private.UpdateCraftsQueryWithFilters(checkbox:GetParentElement():GetParentElement())
end
function private.CraftsOnRowClick(scrollingTable, record, mouseButton)
if mouseButton == "LeftButton" then
TSM.Crafting.Queue.Add(record:GetField("spellId"), 1)
elseif mouseButton == "RightButton" then
TSM.Crafting.Queue.Remove(record:GetField("spellId"), 1)
end
scrollingTable:Draw()
end
function private.MatsInputOnValueChanged(input)
private.UpdateMatsQueryWithFilters(input:GetParentElement():GetParentElement())
end
function private.MatsDropdownOnSelectionChanged(dropdown)
private.UpdateMatsQueryWithFilters(dropdown:GetParentElement():GetParentElement())
end
function private.MatsOnRowClick(scrollingTable, row)
local itemString = row:GetField("itemString")
local priceStr = private.settings.mats[itemString].customValue or private.settings.defaultMatCostMethod
scrollingTable:GetBaseElement():ShowDialogFrame(UIElements.New("Frame", "frame")
:SetLayout("VERTICAL")
:SetSize(478, 312)
:SetPadding(12)
:AddAnchor("CENTER")
:SetBackgroundColor("FRAME_BG", true)
:SetContext(itemString)
:AddChild(UIElements.New("Frame", "header")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(0, 0, -4, 10)
:AddChild(UIElements.New("Spacer", "spacer")
:SetWidth(20)
)
:AddChild(UIElements.New("Text", "title")
:SetJustifyH("CENTER")
:SetFont("BODY_BODY1_BOLD")
:SetText(L["Edit Material Price"])
)
:AddChild(UIElements.New("Button", "closeBtn")
:SetMargin(0, -4, 0, 0)
:SetBackgroundAndSize("iconPack.24x24/Close/Default")
:SetScript("OnClick", private.DialogCloseBtnOnClick)
)
)
:AddChild(UIElements.New("Frame", "item")
:SetLayout("HORIZONTAL")
:SetPadding(6)
:SetMargin(0, 0, 0, 10)
:SetBackgroundColor("PRIMARY_BG_ALT", true)
:AddChild(UIElements.New("Button", "icon")
:SetSize(36, 36)
:SetMargin(0, 8, 0, 0)
:SetBackground(ItemInfo.GetTexture(itemString))
:SetTooltip(itemString)
)
:AddChild(UIElements.New("Text", "name")
:SetHeight(36)
:SetFont("ITEM_BODY1")
:SetText(TSM.UI.GetColoredItemName(itemString))
)
)
:AddChild(UIElements.New("Text", "desc")
:SetHeight(20)
:SetMargin(0, 0, 0, 6)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Material Price"])
)
:AddChild(UIElements.New("MultiLineInput", "input")
:SetMargin(0, 0, 0, 12)
:SetBackgroundColor("PRIMARY_BG_ALT")
:SetFont("BODY_BODY2_MEDIUM")
:SetValidateFunc("CUSTOM_PRICE")
:SetValue(Money.ToString(priceStr) or priceStr)
:SetScript("OnValueChanged", private.MatPriceInputOnValueChanged)
)
:AddChild(UIElements.New("Frame", "buttons")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Button", "resetBtn")
:SetWidth("AUTO")
:SetFont("BODY_BODY3_MEDIUM")
:SetTextColor(private.settings.mats[itemString].customValue and "TEXT" or "TEXT_ALT")
:SetDisabled(not private.settings.mats[itemString].customValue)
:SetText(L["Reset to Default"])
:SetScript("OnClick", private.ResetButtonOnClick)
)
:AddChild(UIElements.New("Frame", "spacer"))
:AddChild(UIElements.New("ActionButton", "closeBtn")
:SetWidth(342)
:SetText(L["Save"])
:SetScript("OnClick", private.DialogCloseBtnOnClick)
)
)
)
end
function private.DialogCloseBtnOnClick(button)
button:GetBaseElement():HideDialog()
end
function private.MatPriceInputOnValueChanged(input)
local value = input:GetValue()
local itemString = input:GetParentElement():GetContext()
TSM.Crafting.SetMatCustomValue(itemString, value)
input:GetElement("__parent.buttons.resetBtn")
:SetTextColor("TEXT")
:SetDisabled(false)
:Draw()
end
function private.ResetButtonOnClick(button)
local itemString = button:GetParentElement():GetParentElement():GetContext()
TSM.Crafting.SetMatCustomValue(itemString, nil)
assert(not private.settings.mats[itemString].customValue)
button:SetTextColor("TEXT_ALT")
button:SetDisabled(true)
button:Draw()
button:GetElement("__parent.__parent.input")
:SetValue(Money.ToString(private.settings.defaultMatCostMethod) or private.settings.defaultMatCostMethod)
:Draw()
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.FirstOperationVirtualField(itemString)
return TSM.Operations.GetFirstOperationByItem("Crafting", itemString) or ""
end
function private.UpdateCraftsQueryWithFilters(frame)
private.craftsQuery:ResetFilters()
-- apply search filter
local filter = strtrim(frame:GetElement("search.input"):GetValue())
if filter ~= "" then
private.craftsQuery:Matches("itemName", String.Escape(filter))
end
-- apply dropdown filter
local professionPlayer = frame:GetElement("profession.dropdown"):GetSelectedItem()
if professionPlayer ~= private.craftProfessions[1] then
local profession, player = strmatch(professionPlayer, "^(.+) %- ([^ ]+)$")
private.craftsQuery
:Equal("profession", profession)
:Or()
:Equal("players", player)
:Matches("players", "^"..player..",")
:Matches("players", ","..player..",")
:Matches("players", ","..player.."$")
:End()
end
-- apply craftable filter
local craftableOnly = frame:GetElement("craftable.checkbox"):IsChecked()
if craftableOnly then
private.craftsQuery:Custom(private.IsCraftableQueryFilter)
end
frame:GetElement("__parent.crafts"):SetQuery(private.craftsQuery, true)
end
function private.IsCraftableQueryFilter(record)
return TSM.Crafting.ProfessionUtil.GetNumCraftableFromDB(record:GetField("spellId")) > 0
end
function private.UpdateMatsQueryWithFilters(frame)
private.matsQuery:ResetFilters()
-- apply search filter
local filter = strtrim(frame:GetElement("search.input"):GetValue())
if filter ~= "" then
private.matsQuery:Custom(private.MatItemNameQueryFilter, strlower(String.Escape(filter)))
end
-- apply dropdown filters
local profession = frame:GetElement("profession.dropdown"):GetSelectedItem()
if profession ~= private.matProfessions[1] then
private.matsQuery
:Or()
:Equal("professions", profession)
:Matches("professions", "^"..profession..",")
:Matches("professions", ","..profession..",")
:Matches("professions", ","..profession.."$")
:End()
end
local priceSource = frame:GetElement("priceSource.dropdown"):GetSelectedItem()
if priceSource == MAT_PRICE_SOURCES[2] then
private.matsQuery:Equal("customValue", "")
elseif priceSource == MAT_PRICE_SOURCES[3] then
private.matsQuery:NotEqual("customValue", "")
end
frame:GetElement("__parent.mats"):SetQuery(private.matsQuery, true)
end
function private.MatItemNameQueryFilter(row, filter)
local name = ItemInfo.GetName(row:GetField("itemString"))
if not name then return end
return strmatch(strlower(name), filter)
end
function private.CraftsMatsMenuIterator(scrollingTable, prevIndex)
if prevIndex == "CREATE_GROUPS" then
-- we're done
return
else
return "CREATE_GROUPS", L["Create Groups from Table"]
end
end
function private.CraftsMenuClickHandler(scrollingTable, index1, index2)
if index1 == "CREATE_GROUPS" then
assert(not index2)
scrollingTable:GetBaseElement():HideDialog()
local numCreated, numAdded = 0, 0
for _, row in private.craftsQuery:Iterator() do
local itemString = row:GetField("itemString")
local groupPath = TSM.Groups.Path.Join(L["Crafted Items"], row:GetField("profession"))
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
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.MatsMenuClickHandler(scrollingTable, index1, index2)
if index1 == "CREATE_GROUPS" then
assert(not index2)
scrollingTable:GetBaseElement():HideDialog()
local numCreated, numAdded = 0, 0
for _, row in private.matsQuery:Iterator() do
local itemString = row:GetField("itemString")
local groupPath = L["Materials"]
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
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

View File

@@ -0,0 +1,354 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Gathering = TSM.UI.CraftingUI:NewPackage("Gathering")
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
local Table = TSM.Include("Util.Table")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
local private = {
settings = nil,
frame = nil,
query = nil,
}
local SOURCE_LIST = {
"vendor",
"guildBank",
"alt",
"altGuildBank",
"craftProfit",
"craftNoProfit",
"auction",
"auctionDE",
"auctionCrafting"
}
local SOURCE_TEXT_LIST = {
L["Vendor"],
L["Guild Bank"],
L["Alts"],
L["Alt Guild Bank"],
L["Craft (Profitable)"],
L["Craft (Unprofitable)"],
L["AH"],
L["AH (Disenchanting)"],
L["AH (Crafting)"],
}
if TSM.IsWowClassic() then
Table.RemoveByValue(SOURCE_LIST, "guildBank")
Table.RemoveByValue(SOURCE_LIST, "altGuildBank")
Table.RemoveByValue(SOURCE_TEXT_LIST, L["Guild Bank"])
Table.RemoveByValue(SOURCE_TEXT_LIST, L["Alt Guild Bank"])
end
assert(#SOURCE_LIST == #SOURCE_TEXT_LIST)
-- ============================================================================
-- Module Functions
-- ============================================================================
function Gathering.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "craftingUIContext", "gatheringDividedContainer")
:AddKey("global", "craftingUIContext", "gatheringScrollingTable")
:AddKey("profile", "gatheringOptions", "sources")
TSM.UI.CraftingUI.RegisterTopLevelPage(L["Gathering"], private.GetGatheringFrame)
TSM.Crafting.Gathering.SetContextChangedCallback(private.ContextChangedCallback)
TSM.UI.TaskListUI.RegisterUpdateCallback(private.UpdateButtonState)
end
-- ============================================================================
-- Gathering UI
-- ============================================================================
function private.GetGatheringFrame()
TSM.UI.AnalyticsRecordPathChange("crafting", "gathering")
assert(not private.query)
private.query = TSM.Crafting.Gathering.CreateQuery()
:SetUpdateCallback(private.UpdateButtonState)
local frame = UIElements.New("DividedContainer", "gathering")
:SetMinWidth(284, 200)
:SetBackgroundColor("PRIMARY_BG")
:SetSettingsContext(private.settings, "gatheringDividedContainer")
:SetLeftChild(UIElements.New("ScrollFrame", "setup")
:SetPadding(12)
:SetBackgroundColor("PRIMARY_BG_ALT")
:AddChild(UIElements.New("Text", "title")
:SetHeight(20)
:SetMargin(0, 0, 0, 8)
:SetFont("BODY_BODY1_BOLD")
:SetText(L["Gathering Setup"])
)
:AddChild(UIElements.New("Text", "crafterDropdownLabel")
:SetHeight(20)
:SetMargin(0, 0, 0, 2)
:SetFont("BODY_BODY3_MEDIUM")
:SetTextColor("INDICATOR")
:SetText(L["Crafter"])
)
:AddChild(UIElements.New("SelectionDropdown", "crafterDropdown")
:SetHeight(24)
:SetMargin(0, 0, 0, 8)
:SetHintText(L["Select crafter"])
:SetScript("OnSelectionChanged", private.CrafterDropdownOnSelectionChanged)
)
:AddChild(UIElements.New("Text", "professionDropdownLabel")
:SetHeight(20)
:SetMargin(0, 0, 0, 2)
:SetFont("BODY_BODY3_MEDIUM")
:SetTextColor("INDICATOR")
:SetText(L["Profession"])
)
:AddChild(UIElements.New("MultiselectionDropdown", "professionDropdown")
:SetHeight(24)
:SetMargin(0, 0, 0, 24)
:SetHintText(L["Select professions"])
:SetSelectionText(L["No Professions"], L["%d Professions"], L["All Professions"])
:SetScript("OnSelectionChanged", private.ProfessionDropdownOnSelectionChanged)
)
:AddChild(UIElements.New("Text", "sourcesCategoryText")
:SetHeight(20)
:SetMargin(0, 0, 0, 2)
:SetFont("BODY_BODY3_MEDIUM")
:SetTextColor("INDICATOR")
:SetText(L["Sources"])
)
:AddChild(UIElements.New("Text", "sourcesDesc")
:SetHeight(28)
:SetMargin(0, 0, 0, 4)
:SetFont("BODY_BODY3_MEDIUM")
:SetTextColor("TEXT_ALT")
:SetText(L["Define what priority Gathering gives certain sources."])
)
:AddChildrenWithFunction(private.CreateSourceRows)
)
:SetRightChild(UIElements.New("Frame", "mats")
:SetLayout("VERTICAL")
:SetBackgroundColor("PRIMARY_BG_ALT")
:AddChild(UIElements.New("Text", "title")
:SetHeight(20)
:SetMargin(0, 0, 8, 8)
:SetFont("BODY_BODY1_BOLD")
:SetJustifyH("CENTER")
:SetText(L["Materials to Gather"])
)
:AddChild(UIElements.New("QueryScrollingTable", "table")
:SetSettingsContext(private.settings, "gatheringScrollingTable")
:GetScrollingTableInfo()
:NewColumn("name")
:SetTitle(NAME)
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetIconSize(12)
:SetTextInfo("itemString", TSM.UI.GetColoredItemName)
:SetIconInfo("itemString", ItemInfo.GetTexture)
:SetTooltipInfo("itemString")
:SetSortInfo("name")
:DisableHiding()
:Commit()
:NewColumn("sources")
:SetTitle(L["Sources"])
:SetFont("BODY_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("sourcesStr", private.MatsGetSourcesStrText)
:SetSortInfo("sourcesStr")
:Commit()
:NewColumn("have")
:SetTitle(L["Have"])
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextInfo("numHave")
:SetSortInfo("numHave")
:Commit()
:NewColumn("need")
:SetTitle(NEED)
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextInfo("numNeed")
:SetSortInfo("numNeed")
:Commit()
:Commit()
:SetQuery(TSM.Crafting.Gathering.CreateQuery()
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
:OrderBy("name", true)
)
:SetSelectionDisabled(true)
:SetAutoReleaseQuery(true)
)
:AddChild(UIElements.New("Texture", "headerTopLine")
:SetHeight(2)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("ActionButton", "openTaskListBtn")
:SetHeight(26)
:SetMargin(8)
:SetScript("OnClick", TSM.UI.TaskListUI.Toggle)
)
)
:SetScript("OnUpdate", private.FrameOnUpdate)
:SetScript("OnHide", private.FrameOnHide)
private.frame = frame
return frame
end
function private.MatsGetSourcesStrText(str)
str = gsub(str, "/[^,]+", "")
for i = 1, #SOURCE_LIST do
str = gsub(str, SOURCE_LIST[i], SOURCE_TEXT_LIST[i])
end
return str
end
function private.CreateSourceRows(frame)
for i = 1, #SOURCE_LIST do
frame:AddChild(UIElements.New("Frame", "sourceFrame"..i)
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(0, 0, 8, 0)
:AddChild(UIElements.New("Text", "label")
:SetFont("BODY_BODY3_MEDIUM")
:SetTextColor((i > #private.settings.sources + 1) and "TEXT_ALT+DISABLED" or "TEXT_ALT")
:SetFormattedText(L["SOURCE %d"], i)
)
:AddChild(UIElements.New("SelectionDropdown", "dropdown")
:SetWidth(188)
:SetContext(i)
:SetHintText(L["Select a Source"])
:SetScript("OnSelectionChanged", private.SourceDropdownOnSelectionChanged)
)
)
end
private.UpdateSourceRows(frame)
end
function private.UpdateSourceRows(setupFrame)
if TSM.IsWowClassic() then
Table.RemoveByValue(private.settings.sources, "guildBank")
Table.RemoveByValue(private.settings.sources, "altGuildBank")
end
local texts = TempTable.Acquire()
local sources = TempTable.Acquire()
for i = 1, #SOURCE_LIST do
wipe(texts)
wipe(sources)
for j = 1, #SOURCE_LIST do
local index = Table.KeyByValue(private.settings.sources, SOURCE_LIST[j])
if not index or index >= i then
tinsert(texts, SOURCE_TEXT_LIST[j])
tinsert(sources, SOURCE_LIST[j])
end
end
if i <= #private.settings.sources then
tinsert(texts, "<"..strupper(REMOVE)..">")
tinsert(sources, "")
end
setupFrame:GetElement("sourceFrame"..i..".label")
:SetTextColor((i > #private.settings.sources + 1) and "TEXT_ALT+DISABLED" or "TEXT_ALT")
setupFrame:GetElement("sourceFrame"..i..".dropdown")
:SetItems(texts, sources)
:SetDisabled(i > #private.settings.sources + 1)
:SetHintText(L["Select a Source"])
:SetSelectedItemByKey(private.settings.sources[i], true)
end
TempTable.Release(texts)
TempTable.Release(sources)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.FrameOnUpdate(frame)
private.UpdateButtonState()
frame:SetScript("OnUpdate", nil)
private.ContextChangedCallback()
end
function private.FrameOnHide(frame)
assert(frame == private.frame)
private.frame = nil
private.query:Release()
private.query = nil
end
function private.CrafterDropdownOnSelectionChanged(dropdown)
TSM.Crafting.Gathering.SetCrafter(dropdown:GetSelectedItem() or "")
dropdown:GetElement("__parent.professionDropdown")
:SetItems(TSM.Crafting.Gathering.GetProfessionList())
:SetSelectedItems(TSM.Crafting.Gathering.GetProfessions())
:Draw()
end
function private.ProfessionDropdownOnSelectionChanged(dropdown)
local professions = TempTable.Acquire()
dropdown:GetSelectedItems(professions)
TSM.Crafting.Gathering.SetProfessions(professions)
TempTable.Release(professions)
end
function private.SourceDropdownOnSelectionChanged(dropdown)
local index = dropdown:GetContext()
local source = dropdown:GetSelectedItemKey()
if source == "" then
tremove(private.settings.sources, index)
else
private.settings.sources[index] = source
for i = #private.settings.sources, index + 1, -1 do
if private.settings.sources[i] == source then
tremove(private.settings.sources, i)
end
end
end
local setupFrame = dropdown:GetParentElement():GetParentElement()
private.UpdateSourceRows(setupFrame)
setupFrame:Draw()
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.ContextChangedCallback()
if not private.frame then
return
end
private.frame:GetElement("setup.crafterDropdown")
:SetItems(TSM.Crafting.Gathering.GetCrafterList())
:SetSelectedItem(TSM.Crafting.Gathering.GetCrafter())
:Draw()
private.frame:GetElement("setup.professionDropdown")
:SetItems(TSM.Crafting.Gathering.GetProfessionList())
:SetSelectedItems(TSM.Crafting.Gathering.GetProfessions())
:Draw()
end
function private.UpdateButtonState()
if not private.frame then
return
end
local button = private.frame:GetElement("mats.openTaskListBtn")
if private.query:Count() == 0 then
button:SetText(L["No Materials to Gather"])
button:SetDisabled(true)
elseif TSM.UI.TaskListUI.IsVisible() then
button:SetText(L["Tasks Added to Task List"])
button:SetDisabled(true)
else
button:SetText(L["Open Task List"])
button:SetDisabled(false)
end
button:Draw()
end

289
Core/UI/Debug/DBViewer.lua Normal file
View File

@@ -0,0 +1,289 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local DBViewer = TSM.UI:NewPackage("DBViewer")
local Database = TSM.Include("Util.Database")
local Log = TSM.Include("Util.Log")
local UIElements = TSM.Include("UI.UIElements")
local private = {
frame = nil,
frameContext = {},
dividedContainerContext = {},
selectedDBName = nil,
structureScrollingTableContext = {},
browseScrollingTableContext = {},
defaultBrowseScrollingTableContext = { colWidth = {}, colHidden = {} },
}
local DEFAULT_FRAME_CONTEXT = {
width = 900,
height = 600,
centerX = 100,
centerY = 0,
scale = 1,
}
local MIN_FRAME_SIZE = {
width = 900,
height = 600
}
local DEFAULT_DIVIDED_CONTAINER_CONTEXT = {
leftWidth = 200,
}
local DEFAULT_STRUCTURE_SCROLLING_TABLE_CONTEXT = {
colWidth = {
order = 24,
field = 450,
type = 60,
attributes = 96,
},
colHidden = {},
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function DBViewer.OnDisable()
-- hide the frame
if private.frame then
DBViewer.Toggle()
end
end
function DBViewer.Toggle()
if not private.frame then
private.frame = private.CreateMainFrame()
private.frame:Draw()
private.frame:Show()
else
private.frame:Hide()
assert(not private.frame)
end
end
-- ============================================================================
-- UI Functions
-- ============================================================================
function private.CreateMainFrame()
private.selectedDBName = nil
return UIElements.New("ApplicationFrame", "base")
:SetParent(UIParent)
:SetMinResize(MIN_FRAME_SIZE.width, MIN_FRAME_SIZE.height)
:SetContextTable(private.frameContext, DEFAULT_FRAME_CONTEXT)
:SetStrata("HIGH")
:SetTitle("TSM DB Viewer")
:SetScript("OnHide", private.FrameOnHide)
:SetContentFrame(UIElements.New("DividedContainer", "container")
:SetContextTable(private.dividedContainerContext, DEFAULT_DIVIDED_CONTAINER_CONTEXT)
:SetBackgroundColor("PRIMARY_BG")
:SetMinWidth(100, 100)
:SetLeftChild(UIElements.New("ScrollFrame", "left")
:AddChildrenWithFunction(private.AddTableRows)
)
:SetRightChild(UIElements.New("Frame", "right")
:SetLayout("VERTICAL")
)
)
end
function private.AddTableRows(frame)
for _, name in Database.InfoNameIterator() do
frame:AddChild(UIElements.New("Button", "nav_"..name)
:SetHeight(20)
:SetPadding(8, 0, 0, 0)
:SetFont("BODY_BODY3")
:SetJustifyH("LEFT")
:SetHighlightEnabled(true)
:SetText(name)
:SetBackground("PRIMARY_BG")
:SetScript("OnClick", private.NavButtonOnClick)
)
end
end
function private.CreateTableContent()
local contentFrame = private.frame:GetElement("container.right")
contentFrame:ReleaseAllChildren()
contentFrame:AddChild(UIElements.New("TabGroup", "tabs")
:SetMargin(0, 0, 4, 0)
:SetNavCallback(private.ContentNavCallback)
:AddPath("Structure", true)
:AddPath("Browse")
)
contentFrame:Draw()
end
function private.ContentNavCallback(_, path)
if path == "Structure" then
return private.CreateStructureFrame()
elseif path == "Browse" then
return private.CreateBrowseFrame()
else
error("Invalid path: "..tostring(path))
end
end
function private.CreateStructureFrame()
local query = Database.CreateInfoFieldQuery(private.selectedDBName)
:OrderBy("order", true)
return UIElements.New("Frame", "structure")
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Frame", "info")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(4)
:AddChild(UIElements.New("Text", "numRows")
:SetFont("BODY_BODY2")
:SetText("Total Rows: "..Database.GetNumRows(private.selectedDBName))
)
:AddChild(UIElements.New("Text", "numRows")
:SetFont("BODY_BODY2")
:SetText("Active Queries: "..Database.GetNumActiveQueries(private.selectedDBName))
)
)
:AddChild(UIElements.New("QueryScrollingTable", "table")
:SetContextTable(private.structureScrollingTableContext, DEFAULT_STRUCTURE_SCROLLING_TABLE_CONTEXT)
:GetScrollingTableInfo()
:NewColumn("order")
:SetTitle("#")
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("order")
:SetSortInfo("order")
:Commit()
:NewColumn("field")
:SetTitle("Field")
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("field")
:SetSortInfo("field")
:Commit()
:NewColumn("type")
:SetTitle("Type")
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("type")
:SetSortInfo("type")
:Commit()
:NewColumn("attributes")
:SetTitle("Attributes")
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("attributes")
:SetSortInfo("attributes")
:Commit()
:Commit()
:SetQuery(query)
:SetAutoReleaseQuery(true)
:SetSelectionDisabled(true)
)
end
function private.CreateBrowseFrame()
local query = Database.CreateDBQuery(private.selectedDBName)
local fieldQuery = Database.CreateInfoFieldQuery(private.selectedDBName)
:Select("field")
:OrderBy("order", true)
wipe(private.defaultBrowseScrollingTableContext.colWidth)
for _, field in fieldQuery:Iterator() do
private.defaultBrowseScrollingTableContext.colWidth[field] = 100
end
wipe(private.browseScrollingTableContext)
local frame = UIElements.New("Frame", "browse")
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Input", "queryInput")
:SetHeight(26)
:SetMargin(8)
:SetBackgroundColor("PRIMARY_BG_ALT")
:SetValue("query")
:SetScript("OnEnterPressed", private.QueryInputOnEnterPressed)
)
:AddChild(UIElements.New("QueryScrollingTable", "table")
:SetContextTable(private.browseScrollingTableContext, private.defaultBrowseScrollingTableContext)
:SetContext(query)
:SetQuery(query)
:SetAutoReleaseQuery(true)
:SetSelectionDisabled(true)
)
local stInfo = frame:GetElement("table"):GetScrollingTableInfo()
for _, field in fieldQuery:Iterator() do
stInfo:NewColumn(field)
:SetTitle(field)
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo(field, tostring)
:SetTooltipInfo(field, private.TooltipFunc)
:Commit()
end
fieldQuery:Release()
stInfo:Commit()
return frame
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.FrameOnHide(frame)
assert(frame == private.frame)
private.frame:Release()
private.frame = nil
end
function private.NavButtonOnClick(button)
local navFrame = button:GetParentElement()
private.selectedDBName = button:GetText()
for _, name in Database.InfoNameIterator() do
navFrame:GetElement("nav_"..name)
:SetTextColor(name == private.selectedDBName and "INDICATOR" or "TEXT")
:Draw()
end
private.CreateTableContent()
end
function private.QueryInputOnEnterPressed(input)
local func, errStr = loadstring(input:GetValue())
if not func then
Log.PrintfUser("Failed to compile code: "..errStr)
return
end
local tableElement = input:GetElement("__parent.table")
local query = tableElement:GetContext()
query:Reset()
setfenv(func, { query = query })
local ok, funcErrStr = pcall(func)
if not ok then
Log.PrintfUser("Failed to execute code: "..funcErrStr)
return
end
tableElement:UpdateData(true)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.TooltipFunc(value)
value = tostring(value)
if strmatch(value, "item:") or strmatch(value, "battlepet:") or strmatch(value, "[ip]:") then
-- this is an item string or item link
return value
else
return "Value: "..value
end
end

View File

@@ -0,0 +1,455 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local DestroyingUI = TSM.UI:NewPackage("DestroyingUI")
local L = TSM.Include("Locale").GetTable()
local DisenchantInfo = TSM.Include("Data.DisenchantInfo")
local FSM = TSM.Include("Util.FSM")
local ItemString = TSM.Include("Util.ItemString")
local Log = TSM.Include("Util.Log")
local TempTable = TSM.Include("Util.TempTable")
local Theme = TSM.Include("Util.Theme")
local Event = TSM.Include("Util.Event")
local Delay = TSM.Include("Util.Delay")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Conversions = TSM.Include("Service.Conversions")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
local private = {
enterWorldTime = nil,
settings = nil,
fsm = nil,
query = nil,
}
local MIN_FRAME_SIZE = { width = 280, height = 280 }
-- ============================================================================
-- Module Functions
-- ============================================================================
function DestroyingUI.OnInitialize()
Event.Register("PLAYER_ENTERING_WORLD", function()
private.enterWorldTime = GetTime()
end)
private.settings = Settings.NewView()
:AddKey("global", "destroyingUIContext", "frame")
:AddKey("global", "destroyingUIContext", "itemsScrollingTable")
:AddKey("global", "destroyingOptions", "autoShow")
:AddKey("global", "destroyingOptions", "autoStack")
private.FSMCreate()
end
function DestroyingUI.OnDisable()
-- hide the frame
private.fsm:ProcessEvent("EV_FRAME_HIDE")
end
function DestroyingUI.Toggle()
private.fsm:ProcessEvent("EV_FRAME_TOGGLE")
end
-- ============================================================================
-- Main Frame
-- ============================================================================
function private.CreateMainFrame()
TSM.UI.AnalyticsRecordPathChange("destroying")
local frame = UIElements.New("ApplicationFrame", "base")
:SetParent(UIParent)
:SetSettingsContext(private.settings, "frame")
:SetMinResize(MIN_FRAME_SIZE.width, MIN_FRAME_SIZE.height)
:SetStrata("HIGH")
:SetTitle(L["Destroying"])
:SetScript("OnHide", private.FrameOnHide)
:SetContentFrame(UIElements.New("Frame", "content")
:SetLayout("VERTICAL")
:SetBackgroundColor("PRIMARY_BG_ALT")
:AddChild(UIElements.New("Frame", "item")
:SetLayout("VERTICAL")
:SetHeight(82)
:SetMargin(8)
:SetBackgroundColor("PRIMARY_BG_ALT", true)
:SetBorderColor("FRAME_BG")
:AddChild(UIElements.New("Frame", "header")
:SetLayout("HORIZONTAL")
:SetPadding(8, 8, 8, 4)
:SetHeight(32)
:SetBackgroundColor("FRAME_BG", true)
:AddChild(UIElements.New("Button", "icon")
:SetSize(20, 20)
:SetMargin(0, 5, 0, 0)
)
:AddChild(UIElements.New("Text", "name")
:SetHeight(20)
:SetFont("ITEM_BODY2")
)
)
-- draw a line along the bottom to hide the rounded corners at the bottom of the header frame
:AddChildNoLayout(UIElements.New("Texture", "line")
:AddAnchor("BOTTOMLEFT", "header")
:AddAnchor("BOTTOMRIGHT", "header")
:SetHeight(4)
:SetTexture("FRAME_BG")
)
:AddChild(UIElements.New("Frame", "container")
:SetLayout("VERTICAL")
:SetPadding(0, 0, 4, 4)
:AddChild(UIElements.New("ScrollFrame", "scroll")
:SetPadding(8, 8, 0, 0)
)
)
)
:AddChild(UIElements.New("QueryScrollingTable", "items")
:SetSettingsContext(private.settings, "itemsScrollingTable")
:GetScrollingTableInfo()
:NewColumn("item")
:SetTitle(L["Item"])
:SetIconSize(12)
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("itemString", TSM.UI.GetColoredItemName)
:SetIconInfo("itemString", ItemInfo.GetTexture)
:SetTooltipInfo("itemString")
:SetSortInfo("name")
:SetTooltipLinkingDisabled(true)
:SetActionIconInfo(1, 12, private.GetHideIcon)
:SetActionIconClickHandler(private.OnHideIconClick)
:DisableHiding()
:Commit()
:NewColumn("num")
:SetTitle("Qty")
:SetFont("TABLE_TABLE1")
:SetJustifyH("CENTER")
:SetTextInfo("quantity")
:SetSortInfo("quantity")
:Commit()
:Commit()
:SetQuery(private.query)
:SetScript("OnSelectionChanged", private.ItemsOnSelectionChanged)
)
:AddChild(UIElements.New("Texture", "lineBottom")
:SetHeight(2)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("ActionButton", "combineBtn")
:SetHeight(26)
:SetMargin(12, 12, 12, 0)
:SetText(L["Combine Partial Stacks"])
:SetScript("OnClick", private.CombineButtonOnClick)
)
:AddChild(UIElements.NewNamed("SecureMacroActionButton", "destroyBtn", "TSMDestroyBtn")
:SetHeight(26)
:SetMargin(12, 12, 8, 12)
:SetText(L["Destroy Next"])
:SetScript("PreClick", private.DestroyButtonPreClick)
)
)
frame:GetElement("titleFrame.closeBtn"):SetScript("OnClick", private.CloseButtonOnClick)
:Draw()
return frame
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.FrameOnHide()
TSM.UI.AnalyticsRecordClose("destroying")
private.fsm:ProcessEvent("EV_FRAME_TOGGLE")
end
function private.GetHideIcon(self, data, iconIndex, isMouseOver)
assert(iconIndex == 1)
-- TODO: needs a new texture for the icon
return true, isMouseOver and TSM.UI.TexturePacks.GetColoredKey("iconPack.12x12/Hide", "TEXT_ALT") or "iconPack.12x12/Hide", true, L["Click to hide this item for the current session. Hold shift to hide this item permanently."]
end
function private.OnHideIconClick(self, data, iconIndex, mouseButton)
assert(iconIndex == 1)
if mouseButton ~= "LeftButton" then
return
end
local row = self._query:GetResultRowByUUID(data)
local itemString = row:GetField("itemString")
if IsShiftKeyDown() then
Log.PrintfUser(L["Destroying will ignore %s permanently. You can remove it from the ignored list in the settings."], ItemInfo.GetName(itemString))
TSM.Destroying.IgnoreItemPermanent(itemString)
else
Log.PrintfUser(L["Destroying will ignore %s until you log out."], ItemInfo.GetName(itemString))
TSM.Destroying.IgnoreItemSession(itemString)
end
if self._query:Count() == 0 then
private.fsm:ProcessEvent("EV_FRAME_TOGGLE")
end
end
function private.GetDestroyInfo(itemString)
local quality = ItemInfo.GetQuality(itemString)
local ilvl = ItemInfo.GetItemLevel(ItemString.GetBase(itemString))
local classId = ItemInfo.GetClassId(itemString)
local info = TempTable.Acquire()
local targetItems = TempTable.Acquire()
for targetItemString in DisenchantInfo.TargetItemIterator() do
local amountOfMats, matRate, minAmount, maxAmount = DisenchantInfo.GetTargetItemSourceInfo(targetItemString, classId, quality, ilvl)
if amountOfMats then
local name = ItemInfo.GetName(targetItemString)
local color = ItemInfo.GetQualityColor(targetItemString)
if name and color then
matRate = matRate and matRate * 100
matRate = matRate and matRate.."% " or ""
local range = (minAmount and maxAmount) and Theme.GetFeedbackColor("YELLOW"):ColorText(minAmount ~= maxAmount and (" ["..minAmount.."-"..maxAmount.."]") or (" ["..minAmount.."]")) or ""
tinsert(info, color..matRate..name.." x"..amountOfMats.."|r"..range)
tinsert(targetItems, targetItemString)
end
end
end
for targetItemString, amountOfMats, matRate, minAmount, maxAmount in Conversions.TargetItemsByMethodIterator(itemString, Conversions.METHOD.PROSPECT) do
local name = ItemInfo.GetName(targetItemString)
local color = ItemInfo.GetQualityColor(targetItemString)
if name and color then
matRate = matRate and matRate * 100
matRate = matRate and matRate.."% " or ""
local range = (minAmount and maxAmount) and Theme.GetFeedbackColor("YELLOW"):ColorText(minAmount ~= maxAmount and (" ["..minAmount.."-"..maxAmount.."]") or (" ["..minAmount.."]")) or ""
tinsert(info, color..matRate..name.." x"..amountOfMats.."|r"..range)
tinsert(targetItems, targetItemString)
end
end
for targetItemString, rate in Conversions.TargetItemsByMethodIterator(itemString, Conversions.METHOD.MILL) do
local name = ItemInfo.GetName(targetItemString)
local color = ItemInfo.GetQualityColor(targetItemString)
if name and color then
tinsert(info, color..ItemInfo.GetName(targetItemString).." x"..rate.."|r")
tinsert(targetItems, targetItemString)
end
end
return info, targetItems
end
function private.ItemsOnSelectionChanged(self)
if not self:GetSelection() then
return
end
local itemString = self:GetSelection():GetField("itemString")
local itemFrame = self:GetElement("__parent.item")
itemFrame:GetElement("header.icon")
:SetBackground(ItemInfo.GetTexture(itemString))
:SetTooltip(itemString)
itemFrame:GetElement("header.name")
:SetText(TSM.UI.GetColoredItemName(itemString) or "")
local info, targetItems = private.GetDestroyInfo(itemString)
local scrollFrame = itemFrame:GetElement("container.scroll")
scrollFrame:ReleaseAllChildren()
for i, text in ipairs(info) do
scrollFrame:AddChild(UIElements.New("Button", "row"..i)
:SetHeight(14)
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetText(text)
:SetTooltip(targetItems[i])
)
end
TempTable.Release(info)
TempTable.Release(targetItems)
itemFrame:Draw()
end
function private.CloseButtonOnClick(button)
private.fsm:ProcessEvent("EV_FRAME_TOGGLE")
end
function private.CombineButtonOnClick(button)
button:SetPressed(true)
button:Draw()
private.fsm:ProcessEvent("EV_COMBINE_BUTTON_CLICKED")
end
function private.DestroyButtonPreClick(button)
button:SetPressed(true)
button:Draw()
private.fsm:ProcessEvent("EV_DESTROY_BUTTON_PRE_CLICK")
end
-- ============================================================================
-- FSM
-- ============================================================================
function private.FSMCreate()
TSM.Destroying.SetBagUpdateCallback(private.FSMBagUpdate)
local fsmContext = {
frame = nil,
combineFuture = nil,
destroyFuture = nil,
didShowOnce = false,
didAutoCombine = false,
}
local function UpdateDestroyingFrame(context, noDraw)
if not context.frame then
return
end
local combineBtn = context.frame:GetElement("content.combineBtn")
combineBtn:SetText(context.combineFuture and L["Combining..."] or L["Combine Partial Stacks"])
combineBtn:SetDisabled(context.combineFuture or context.destroyFuture or not TSM.Destroying.CanCombine())
local destroyBtn = context.frame:GetElement("content.destroyBtn")
destroyBtn:SetText(context.destroyFuture and L["Destroying..."] or L["Destroy Next"])
destroyBtn:SetDisabled(context.combineFuture or context.destroyFuture or private.query:Count() == 0)
if not noDraw then
context.frame:Draw()
end
end
private.fsm = FSM.New("DESTROYING")
:AddState(FSM.NewState("ST_FRAME_CLOSED")
:SetOnEnter(function(context)
if context.frame then
context.frame:Hide()
context.frame:Release()
context.frame = nil
end
if context.combineFuture then
context.combineFuture:Cancel()
context.combineFuture = nil
end
if context.destroyFuture then
context.destroyFuture:Cancel()
context.destroyFuture = nil
end
context.didAutoCombine = false
end)
:AddTransition("ST_FRAME_OPENING")
:AddEventTransition("EV_FRAME_TOGGLE", "ST_FRAME_OPENING")
:AddEvent("EV_BAG_UPDATE", function(context)
if not context.didShowOnce and private.settings.autoShow then
return "ST_FRAME_OPENING"
end
end)
)
:AddState(FSM.NewState("ST_FRAME_OPENING")
:SetOnEnter(function(context)
private.query = private.query or TSM.Destroying.CreateBagQuery()
private.query:ResetOrderBy()
private.query:OrderBy("name", true)
if (not private.settings.autoStack or not TSM.Destroying.CanCombine()) and private.query:Count() == 0 then
-- nothing to destroy or combine, so bail
return "ST_FRAME_CLOSED"
end
context.didShowOnce = true
context.frame = private.CreateMainFrame()
context.frame:Show()
private.ItemsOnSelectionChanged(context.frame:GetElement("content.items"))
return "ST_FRAME_OPEN"
end)
:AddTransition("ST_FRAME_OPEN")
:AddTransition("ST_FRAME_CLOSED")
)
:AddState(FSM.NewState("ST_FRAME_OPEN")
:SetOnEnter(function(context)
UpdateDestroyingFrame(context)
if not context.frame:GetElement("content.items"):GetSelection() then
-- select the first row
local result = private.query:GetFirstResult()
context.frame:GetElement("content.items"):SetSelection(result and result:GetUUID() or nil)
end
if private.settings.autoStack and not context.didAutoCombine and TSM.Destroying.CanCombine() then
context.didAutoCombine = true
context.frame:GetElement("content.combineBtn")
:SetPressed(true)
:Draw()
return "ST_COMBINING_STACKS"
elseif not TSM.Destroying.CanCombine() and private.query:Count() == 0 then
-- nothing left to destroy or combine
return "ST_FRAME_CLOSED"
end
context.didAutoCombine = true
end)
:AddTransition("ST_FRAME_OPEN")
:AddTransition("ST_COMBINING_STACKS")
:AddTransition("ST_DESTROYING")
:AddTransition("ST_FRAME_CLOSED")
:AddEventTransition("EV_COMBINE_BUTTON_CLICKED", "ST_COMBINING_STACKS")
:AddEventTransition("EV_DESTROY_BUTTON_PRE_CLICK", "ST_DESTROYING")
:AddEventTransition("EV_BAG_UPDATE", "ST_FRAME_OPEN")
:AddEventTransition("EV_FRAME_HIDE", "ST_FRAME_CLOSED")
)
:AddState(FSM.NewState("ST_COMBINING_STACKS")
:SetOnEnter(function(context)
assert(not context.combineFuture)
context.combineFuture = TSM.Destroying.StartCombine()
context.combineFuture:SetScript("OnDone", private.FSMCombineFutureOnDone)
UpdateDestroyingFrame(context, true)
end)
:AddTransition("ST_COMBINING_DONE")
:AddTransition("ST_FRAME_CLOSED")
:AddEventTransition("EV_COMBINE_DONE", "ST_COMBINING_DONE")
:AddEventTransition("EV_FRAME_HIDE", "ST_FRAME_CLOSED")
)
:AddState(FSM.NewState("ST_COMBINING_DONE")
:SetOnEnter(function(context)
-- don't care what the result was
context.combineFuture:GetValue()
context.combineFuture = nil
context.frame:GetElement("content.combineBtn")
:SetPressed(false)
:Draw()
return "ST_FRAME_OPEN"
end)
:AddTransition("ST_FRAME_OPEN")
)
:AddState(FSM.NewState("ST_DESTROYING")
:SetOnEnter(function(context)
assert(not context.destroyFuture)
context.destroyFuture = TSM.Destroying.StartDestroy(context.frame:GetElement("content.destroyBtn"), context.frame:GetElement("content.items"):GetSelection())
context.destroyFuture:SetScript("OnDone", private.FSMDestroyFutureOnDone)
UpdateDestroyingFrame(context, true)
end)
:AddTransition("ST_DESTROYING_DONE")
:AddTransition("ST_FRAME_CLOSED")
:AddEventTransition("EV_DESTROY_DONE", "ST_DESTROYING_DONE")
:AddEventTransition("EV_FRAME_HIDE", "ST_FRAME_CLOSED")
)
:AddState(FSM.NewState("ST_DESTROYING_DONE")
:SetOnEnter(function(context)
-- don't care what the result was
context.destroyFuture:GetValue()
context.destroyFuture = nil
context.isDestroying = false
context.frame:GetElement("content.destroyBtn")
:SetPressed(false)
:Draw()
return "ST_FRAME_OPEN"
end)
:AddTransition("ST_FRAME_OPEN")
)
:AddDefaultEventTransition("EV_FRAME_TOGGLE", "ST_FRAME_CLOSED")
:Init("ST_FRAME_CLOSED", fsmContext)
end
function private.FSMBagUpdate()
if not private.enterWorldTime or private.enterWorldTime == GetTime() then
-- delay for another frame as wow closes special frames in its PLAYER_ENTERING_WORLD handler
Delay.AfterTime("DESTROYING_BAG_UPDATE_DELAY", 0, private.FSMBagUpdate)
return
end
private.fsm:ProcessEvent("EV_BAG_UPDATE")
end
function private.FSMCombineFutureOnDone()
private.fsm:ProcessEvent("EV_COMBINE_DONE")
end
function private.FSMDestroyFutureOnDone()
private.fsm:ProcessEvent("EV_DESTROY_DONE")
end

View File

@@ -0,0 +1,436 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- ActionButton UI Element Class.
-- An action button is a button which uses specific background textures and has a pressed state. It is a subclass of the
-- @{Text} class.
-- @classmod ActionButton
local _, TSM = ...
local ActionButton = TSM.Include("LibTSMClass").DefineClass("ActionButton", TSM.UI.Text)
local NineSlice = TSM.Include("Util.NineSlice")
local Vararg = TSM.Include("Util.Vararg")
local Event = TSM.Include("Util.Event")
local Theme = TSM.Include("Util.Theme")
local Color = TSM.Include("Util.Color")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(ActionButton)
TSM.UI.ActionButton = ActionButton
local private = {}
local ICON_PADDING = 2
local CLICK_COOLDOWN = 0.2
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ActionButton.__init(self, name, isSecure)
local frame = UIElements.CreateFrame(self, "Button", name, nil, isSecure and "SecureActionButtonTemplate" or nil)
ScriptWrapper.Set(frame, isSecure and "PostClick" or "OnClick", private.OnClick, self)
ScriptWrapper.Set(frame, "OnMouseDown", private.OnMouseDown, self)
ScriptWrapper.Set(frame, "OnEnter", private.OnEnter, self)
ScriptWrapper.Set(frame, "OnLeave", private.OnLeave, self)
self.__super:__init(frame)
self._nineSlice = NineSlice.New(frame)
-- create the icon
frame.icon = frame:CreateTexture(nil, "ARTWORK")
frame.icon:SetPoint("RIGHT", frame.text, "LEFT", -ICON_PADDING, 0)
Event.Register("MODIFIER_STATE_CHANGED", function()
if self:IsVisible() and next(self._modifierText) then
self:Draw()
if GameTooltip:IsOwned(self:_GetBaseFrame()) then
self:ShowTooltip(self._tooltip)
end
end
end)
self._iconTexturePack = nil
self._pressed = nil
self._disabled = false
self._locked = false
self._lockedColor = nil
self._justifyH = "CENTER"
self._font = "BODY_BODY2_MEDIUM"
self._defaultText = ""
self._modifierText = {}
self._onClickHandler = nil
self._onEnterHandler = nil
self._onLeaveHandler = nil
self._clickCooldown = nil
self._clickCooldownDisabled = false
self._defaultNoBackground = false
self._isMouseDown = false
self._manualRequired = false
end
function ActionButton.Release(self)
self._iconTexturePack = nil
self._pressed = nil
self._disabled = false
self._locked = false
self._lockedColor = nil
self._defaultText = ""
wipe(self._modifierText)
self._onClickHandler = nil
self._onEnterHandler = nil
self._onLeaveHandler = nil
self._clickCooldown = nil
self._clickCooldownDisabled = false
self._defaultNoBackground = false
self._manualRequired = false
self._isMouseDown = false
local frame = self:_GetBaseFrame()
ScriptWrapper.Clear(frame, "OnUpdate")
frame:Enable()
frame:RegisterForClicks("LeftButtonUp")
frame:UnlockHighlight()
self.__super:Release()
self._justifyH = "CENTER"
self._font = "BODY_BODY2_MEDIUM"
end
--- Sets the icon that shows within the button.
-- @tparam ActionButton self The action button object
-- @tparam[opt=nil] string texturePack A texture pack string to set the icon and its size to
-- @treturn ActionButton The action button object
function ActionButton.SetIcon(self, texturePack)
if texturePack then
assert(TSM.UI.TexturePacks.IsValid(texturePack))
self._iconTexturePack = texturePack
else
self._iconTexturePack = nil
end
return self
end
--- Set the text.
-- @tparam ActionButton self The action button object
-- @tparam ?string|number text The text
-- @treturn ActionButton The action button object
function ActionButton.SetText(self, text)
self.__super:SetText(text)
self._defaultText = self:GetText()
return self
end
--- Sets a script handler.
-- @see Element.SetScript
-- @tparam ActionButton self The action button object
-- @tparam string script The script to register for (currently only supports `OnClick`)
-- @tparam function handler The script handler which will be called with the action button object followed by any
-- arguments to the script
-- @treturn ActionButton The action button object
function ActionButton.SetScript(self, script, handler)
if script == "OnClick" then
self._onClickHandler = handler
elseif script == "OnEnter" then
self._onEnterHandler = handler
elseif script == "OnLeave" then
self._onLeaveHandler = handler
elseif script == "OnMouseDown" or script == "OnMouseUp" then
self.__super:SetScript(script, handler)
else
error("Unknown ActionButton script: "..tostring(script))
end
return self
end
--- Sets a script to propagate to the parent element.
-- @tparam ActionButton self The action button object
-- @tparam string script The script to propagate
-- @treturn ActionButton The action button object
function ActionButton.PropagateScript(self, script)
if script == "OnMouseDown" or script == "OnMouseUp" then
self.__super:PropagateScript(script)
else
error("Cannot propagate ActionButton script: "..tostring(script))
end
return self
end
--- Set whether or not the action button is disabled.
-- @tparam ActionButton self The action button object
-- @tparam boolean disabled Whether or not the action button should be disabled
-- @treturn ActionButton The action button object
function ActionButton.SetDisabled(self, disabled)
self._disabled = disabled
self:_UpdateDisabled()
return self
end
--- Set whether or not the action button is pressed.
-- @tparam ActionButton self The action button object
-- @tparam boolean locked Whether or not to lock the action button's highlight
-- @tparam[opt=nil] string color The locked highlight color as a theme color key
-- @treturn ActionButton The action button object
function ActionButton.SetHighlightLocked(self, locked, color)
self._locked = locked
self._lockedColor = color
if locked then
self:_GetBaseFrame():LockHighlight()
else
self:_GetBaseFrame():UnlockHighlight()
end
return self
end
--- Set whether or not the action button is pressed.
-- @tparam ActionButton self The action button object
-- @tparam boolean pressed Whether or not the action button should be pressed
-- @treturn ActionButton The action button object
function ActionButton.SetPressed(self, pressed)
self._pressed = pressed and private.GetModifierKey(IsShiftKeyDown(), IsControlKeyDown(), IsAltKeyDown()) or nil
self:_UpdateDisabled()
return self
end
--- Disables the default click cooldown to allow the button to be spammed (i.e. for macro-able buttons).
-- @tparam ActionButton self The action button object
-- @treturn ActionButton The action button object
function ActionButton.DisableClickCooldown(self)
self._clickCooldownDisabled = true
return self
end
function ActionButton.SetDefaultNoBackground(self)
self._defaultNoBackground = true
return self
end
function ActionButton.SetModifierText(self, text, ...)
local key = private.GetModifierKey(private.ParseModifiers(...))
assert(key and key ~= "NONE")
self._modifierText[key] = text
return self
end
--- Set whether a manual click (vs. a macro) is required.
-- @tparam ActionButton self The action button object
-- @tparam boolean required Whether or not a manual click is required
-- @treturn ActionButton The action button object
function ActionButton.SetRequireManualClick(self, required)
self._manualRequired = required
return self
end
--- Click on the action button.
-- @tparam ActionButton self The action button object
function ActionButton.Click(self)
local frame = self:_GetBaseFrame()
if frame:IsEnabled() and frame:IsVisible() then
private.OnClick(self)
end
end
function ActionButton.Draw(self)
local maxRank, maxRankKey, numMaxRank = nil, nil, nil
local currentModifier = self._pressed or private.GetModifierKey(IsShiftKeyDown(), IsControlKeyDown(), IsAltKeyDown())
local currentShift, currentControl, currentAlt = private.ParseModifiers(strsplit("-", currentModifier))
for key in pairs(self._modifierText) do
local hasShift, hasControl, hasAlt = private.ParseModifiers(strsplit("-", key))
if (not hasShift or currentShift) and (not hasControl or currentControl) and (not hasAlt or currentAlt) then
-- this key matches the current state
local rank = select("#", strsplit("-", key))
if not maxRank or rank > maxRank then
maxRank = rank
numMaxRank = 1
maxRankKey = key
elseif rank == maxRank then
numMaxRank = numMaxRank + 1
end
end
end
if maxRank then
assert(numMaxRank == 1)
self.__super:SetText(self._modifierText[maxRankKey])
else
self.__super:SetText(self._defaultText)
end
self.__super:Draw()
local frame = self:_GetBaseFrame()
-- set nine-slice and text color depending on the state
local textColor, nineSliceTheme, nineSliceColor = nil, nil, nil
if self._pressed or self._clickCooldown then
textColor = Color.GetFullBlack()
nineSliceTheme = "rounded"
nineSliceColor = Theme.GetColor("INDICATOR")
elseif self._disabled then
textColor = Theme.GetColor("ACTIVE_BG_ALT")
nineSliceTheme = "global"
nineSliceColor = Theme.GetColor("ACTIVE_BG")
elseif self._locked then
textColor = Color.GetFullBlack()
nineSliceTheme = "rounded"
nineSliceColor = Theme.GetColor(self._lockedColor or "ACTIVE_BG+HOVER")
elseif frame:IsMouseOver() then
textColor = Theme.GetColor("TEXT")
nineSliceTheme = "rounded"
nineSliceColor = Theme.GetColor("ACTIVE_BG+HOVER")
else
textColor = self:_GetTextColor()
if not self._defaultNoBackground then
nineSliceTheme = "rounded"
nineSliceColor = Theme.GetColor("ACTIVE_BG")
end
end
frame.text:SetTextColor(textColor:GetFractionalRGBA())
if nineSliceTheme then
self._nineSlice:SetStyle(nineSliceTheme)
self._nineSlice:SetVertexColor(nineSliceColor:GetFractionalRGBA())
else
self._nineSlice:Hide()
end
if self._iconTexturePack then
TSM.UI.TexturePacks.SetTextureAndSize(frame.icon, self._iconTexturePack)
frame.icon:Show()
frame.icon:SetVertexColor(textColor:GetFractionalRGBA())
local xOffset = self:GetText() ~= "" and ((TSM.UI.TexturePacks.GetWidth(self._iconTexturePack) + ICON_PADDING) / 2) or (self:_GetDimension("WIDTH") / 2)
frame.text:ClearAllPoints()
frame.text:SetPoint("TOP", xOffset, 0)
frame.text:SetPoint("BOTTOM", xOffset, 0)
frame.text:SetWidth(frame.text:GetStringWidth())
else
frame.icon:Hide()
frame.text:ClearAllPoints()
frame.text:SetPoint("TOPLEFT")
frame.text:SetPoint("BOTTOMRIGHT")
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function ActionButton._UpdateDisabled(self)
local frame = self:_GetBaseFrame()
if self._disabled or self._pressed or self._clickCooldown then
frame:Disable()
else
frame:Enable()
end
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnClick(self)
if not self._acquired then
return
end
if self._manualRequired and not self._isMouseDown then
return
end
self._isMouseDown = false
if not self._clickCooldownDisabled then
self._clickCooldown = CLICK_COOLDOWN
self:_UpdateDisabled()
ScriptWrapper.Set(self:_GetBaseFrame(), "OnUpdate", private.OnUpdate, self)
end
self:Draw()
if self._onClickHandler then
self:_onClickHandler()
end
end
function private.OnMouseDown(self)
self._isMouseDown = true
end
function private.OnEnter(self)
if self._onEnterHandler then
self:_onEnterHandler()
end
if self._disabled or self._pressed or self._clickCooldown then
return
end
self:Draw()
end
function private.OnLeave(self)
if not self:IsVisible() then
return
end
if self._onLeaveHandler then
self:_onLeaveHandler()
end
if self._disabled or self._pressed or self._clickCooldown then
return
end
self:Draw()
end
function private.OnUpdate(self, elapsed)
self._clickCooldown = self._clickCooldown - elapsed
if self._clickCooldown <= 0 then
ScriptWrapper.Clear(self:_GetBaseFrame(), "OnUpdate")
self._clickCooldown = nil
self:_UpdateDisabled()
self:Draw()
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetModifierKey(hasShift, hasControl, hasAlt)
if hasShift and hasControl and hasAlt then
return "SHIFT-CTRL-ALT"
elseif hasShift and hasControl then
return "SHIFT-CTRL"
elseif hasShift and hasAlt then
return "SHIFT-ALT"
elseif hasShift then
return "SHIFT"
elseif hasControl and hasAlt then
return "CTRL-ALT"
elseif hasControl then
return "CTRL"
elseif hasAlt then
return "ALT"
else
return "NONE"
end
end
function private.ParseModifiers(...)
local hasShift, hasControl, hasAlt, hasNone = false, false, false, false
for _, modifier in Vararg.Iterator(...) do
if modifier == "SHIFT" then
assert(not hasShift and not hasNone)
hasShift = true
elseif modifier == "CTRL" then
assert(not hasControl and not hasNone)
hasControl = true
elseif modifier == "ALT" then
assert(not hasAlt and not hasNone)
hasAlt = true
elseif modifier == "NONE" then
assert(not hasShift and not hasControl and not hasAlt and not hasNone)
hasNone = true
else
error("Invalid modifier: "..tostring(modifier))
end
end
return hasShift, hasControl, hasAlt
end

View File

@@ -0,0 +1,82 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- AlphaAnimatedFrame UI Element Class.
-- An alpha animated frame is a frame which allows for animating its alpha. It is a subclass of the @{Frame} class.
-- @classmod AlphaAnimatedFrame
local _, TSM = ...
local AlphaAnimatedFrame = TSM.Include("LibTSMClass").DefineClass("AlphaAnimatedFrame", TSM.UI.Frame)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(AlphaAnimatedFrame)
TSM.UI.AlphaAnimatedFrame = AlphaAnimatedFrame
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function AlphaAnimatedFrame.__init(self)
self.__super:__init()
local frame = self:_GetBaseFrame()
self._ag = frame:CreateAnimationGroup()
self._ag:SetLooping("BOUNCE")
self._alpha = self._ag:CreateAnimation("Alpha")
end
function AlphaAnimatedFrame.Acquire(self)
self._alpha:SetFromAlpha(1)
self._alpha:SetToAlpha(1)
self._alpha:SetDuration(1)
self.__super:Acquire()
end
function AlphaAnimatedFrame.Release(self)
self._ag:Stop()
self.__super:Release()
end
--- Sets the range of the alpha animation.
-- @tparam AlphaAnimatedFrame self The alpha animated frame object
-- @tparam number fromAlpha The initial alpha value (usually 1)
-- @tparam number toAlpha The end alpha value (between 0 and 1 inclusive)
-- @treturn AlphaAnimatedFrame The alpha animated frame object
function AlphaAnimatedFrame.SetRange(self, fromAlpha, toAlpha)
self._alpha:SetFromAlpha(fromAlpha)
self._alpha:SetToAlpha(toAlpha)
return self
end
--- Sets the duration of the animation.
-- @tparam AlphaAnimatedFrame self The alpha animated frame object
-- @tparam number duration The duration in seconds
-- @treturn AlphaAnimatedFrame The alpha animated frame object
function AlphaAnimatedFrame.SetDuration(self, duration)
self._alpha:SetDuration(duration)
return self
end
--- Sets whether or not the animation is playing.
-- @tparam AlphaAnimatedFrame self The alpha animated frame object
-- @tparam boolean play Whether the animation should be playing or not
-- @treturn AlphaAnimatedFrame The alpha animated frame object
function AlphaAnimatedFrame.SetPlaying(self, play)
if play then
self._ag:Play()
else
self._ag:Stop()
end
return self
end
--- Gets whether or not the animation is playing.
-- @tparam AlphaAnimatedFrame self The alpha animated frame object
-- @treturn boolean Whether the animation is playing
function AlphaAnimatedFrame.IsPlaying(self)
return self._ag:IsPlaying()
end

View File

@@ -0,0 +1,682 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- ApplicationFrame UI Element Class.
-- An application frame is the base frame of all of the TSM UIs. It is a subclass of the @{Frame} class.
-- @classmod ApplicationFrame
local _, TSM = ...
local L = TSM.Include("Locale").GetTable()
local Math = TSM.Include("Util.Math")
local TempTable = TSM.Include("Util.TempTable")
local Color = TSM.Include("Util.Color")
local NineSlice = TSM.Include("Util.NineSlice")
local Table = TSM.Include("Util.Table")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Theme = TSM.Include("Util.Theme")
local Tooltip = TSM.Include("UI.Tooltip")
local UIElements = TSM.Include("UI.UIElements")
local ApplicationFrame = TSM.Include("LibTSMClass").DefineClass("ApplicationFrame", TSM.UI.Frame)
UIElements.Register(ApplicationFrame)
TSM.UI.ApplicationFrame = ApplicationFrame
local private = {
menuDialogContext = {},
}
local SECONDS_PER_HOUR = 60 * 60
local SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR
local CONTENT_FRAME_OFFSET = 8
local DIALOG_RELATIVE_LEVEL = 18
local HEADER_HEIGHT = 40
local MIN_SCALE = 0.3
local DIALOG_OPACITY_PCT = 65
local MIN_ON_SCREEN_PX = 50
local function NoOp() end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ApplicationFrame.__init(self)
self.__super:__init()
self._contentFrame = nil
self._contextTable = nil
self._defaultContextTable = nil
self._isScaling = nil
self._protected = nil
self._minWidth = 0
self._minHeight = 0
self._dialogStack = {}
local frame = self:_GetBaseFrame()
local globalFrameName = tostring(frame)
_G[globalFrameName] = frame
-- insert our frames before other addons (i.e. Skillet) to avoid conflicts
tinsert(UISpecialFrames, 1, globalFrameName)
self._nineSlice = NineSlice.New(frame)
self._nineSlice:SetStyle("outerFrame")
frame.resizeIcon = frame:CreateTexture(nil, "ARTWORK")
frame.resizeIcon:SetPoint("BOTTOMRIGHT")
TSM.UI.TexturePacks.SetTextureAndSize(frame.resizeIcon, "iconPack.14x14/Resize")
frame.resizeBtn = CreateFrame("Button", nil, frame)
frame.resizeBtn:SetAllPoints(frame.resizeIcon)
frame.resizeBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
ScriptWrapper.Set(frame.resizeBtn, "OnEnter", private.ResizeButtonOnEnter, self)
ScriptWrapper.Set(frame.resizeBtn, "OnLeave", private.ResizeButtonOnLeave, self)
ScriptWrapper.Set(frame.resizeBtn, "OnMouseDown", private.ResizeButtonOnMouseDown, self)
ScriptWrapper.Set(frame.resizeBtn, "OnMouseUp", private.ResizeButtonOnMouseUp, self)
ScriptWrapper.Set(frame.resizeBtn, "OnClick", private.ResizeButtonOnClick, self)
Theme.RegisterChangeCallback(function()
if self:IsVisible() then
self:Draw()
end
end)
end
function ApplicationFrame.Acquire(self)
self:AddChildNoLayout(UIElements.New("Frame", "titleFrame")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddAnchor("TOPLEFT", 8, -8)
:AddAnchor("TOPRIGHT", -8, -8)
:SetBackgroundColor("FRAME_BG")
:AddChild(UIElements.New("Texture", "icon")
:SetMargin(0, 16, 0, 0)
:SetTextureAndSize("uiFrames.SmallLogo")
)
:AddChild(UIElements.New("Text", "title")
:AddAnchor("CENTER")
:SetWidth("AUTO")
:SetFont("BODY_BODY2_BOLD")
:SetTextColor("TEXT_ALT")
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Button", "closeBtn")
:SetBackgroundAndSize("iconPack.24x24/Close/Default")
:SetScript("OnClick", private.CloseButtonOnClick)
)
)
self.__super:Acquire()
local frame = self:_GetBaseFrame()
frame:EnableMouse(true)
frame:SetMovable(true)
frame:SetResizable(true)
frame:RegisterForDrag("LeftButton")
self:SetScript("OnDragStart", private.FrameOnDragStart)
self:SetScript("OnDragStop", private.FrameOnDragStop)
end
function ApplicationFrame.Release(self)
if self._protected then
tinsert(UISpecialFrames, 1, tostring(self:_GetBaseFrame()))
end
self._contentFrame = nil
self._contextTable = nil
self._defaultContextTable = nil
self:_GetBaseFrame():SetMinResize(0, 0)
self:_GetBaseFrame():SetMaxResize(0, 0)
self._isScaling = nil
self._protected = nil
self._minWidth = 0
self._minHeight = 0
self.__super:Release()
end
--- Adds player gold text to the title frame.
-- @tparam ApplicationFrame self The application frame object
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.AddPlayerGold(self)
local titleFrame = self:GetElement("titleFrame")
titleFrame:AddChildBeforeById(titleFrame:HasChildById("switchBtn") and "switchBtn" or "closeBtn", UIElements.New("PlayerGoldText", "playerGold")
:SetWidth("AUTO")
:SetMargin(0, 8, 0, 0)
)
return self
end
--- Adds the app status icon to the title frame.
-- @tparam ApplicationFrame self The application frame object
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.AddAppStatusIcon(self)
local color, texture = nil, nil
local appUpdateAge = time() - TSM.GetAppUpdateTime()
local auctionDBRealmTime, auctionDBRegionTime = TSM.AuctionDB.GetAppDataUpdateTimes()
local auctionDBRealmAge = time() - auctionDBRealmTime
local auctionDBRegionAge = time() - auctionDBRegionTime
if appUpdateAge >= 2 * SECONDS_PER_DAY or auctionDBRealmAge > 2 * SECONDS_PER_DAY or auctionDBRegionAge > 2 * SECONDS_PER_DAY then
color = "RED"
texture = "iconPack.14x14/Attention"
elseif appUpdateAge >= 2 * SECONDS_PER_HOUR or auctionDBRealmAge >= 4 * SECONDS_PER_HOUR then
color = "YELLOW"
texture = "iconPack.14x14/Attention"
else
color = "GREEN"
texture = "iconPack.14x14/Checkmark/Circle"
end
local titleFrame = self:GetElement("titleFrame")
titleFrame:AddChildBeforeById("playerGold", UIElements.New("Button", "playerGold")
:SetBackgroundAndSize(TSM.UI.TexturePacks.GetColoredKey(texture, Theme.GetFeedbackColor(color)))
:SetMargin(0, 8, 0, 0)
:SetTooltip(private.GetAppStatusTooltip)
)
return self
end
--- Adds a switch button to the title frame.
-- @tparam ApplicationFrame self The application frame object
-- @tparam function onClickHandler The handler for the OnClick script for the button
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.AddSwitchButton(self, onClickHandler)
local titleFrame = self:GetElement("titleFrame")
titleFrame:AddChildBeforeById("closeBtn", UIElements.New("ActionButton", "switchBtn")
:SetSize(95, 20)
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["WOW UI"])
:SetScript("OnClick", onClickHandler)
)
return self
end
function ApplicationFrame.SetProtected(self, protected)
self._protected = protected
local globalFrameName = tostring(self:_GetBaseFrame())
if protected then
Table.RemoveByValue(UISpecialFrames, globalFrameName)
else
if not Table.KeyByValue(UISpecialFrames, globalFrameName) then
-- insert our frames before other addons (i.e. Skillet) to avoid conflicts
tinsert(UISpecialFrames, 1, globalFrameName)
end
end
return self
end
--- Sets the title text.
-- @tparam ApplicationFrame self The application frame object
-- @tparam string title The title text
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.SetTitle(self, title)
local titleFrame = self:GetElement("titleFrame")
titleFrame:GetElement("title"):SetText(title)
titleFrame:Draw()
return self
end
--- Sets the content frame.
-- @tparam ApplicationFrame self The application frame object
-- @tparam Frame frame The frame's content frame
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.SetContentFrame(self, frame)
assert(frame:__isa(TSM.UI.Frame))
frame:WipeAnchors()
frame:AddAnchor("TOPLEFT", CONTENT_FRAME_OFFSET, -HEADER_HEIGHT)
frame:AddAnchor("BOTTOMRIGHT", -CONTENT_FRAME_OFFSET, CONTENT_FRAME_OFFSET)
frame:SetPadding(2)
frame:SetBorderColor("ACTIVE_BG", 2)
self._contentFrame = frame
self:AddChildNoLayout(frame)
return self
end
--- Sets the context table.
-- This table can be used to preserve position and size information across lifecycles of the application frame and even
-- WoW sessions if it's within the settings DB.
-- @tparam ApplicationFrame self The application frame object
-- @tparam table tbl The context table
-- @tparam table defaultTbl Default values (required attributes: `width`, `height`, `centerX`, `centerY`)
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.SetContextTable(self, tbl, defaultTbl)
assert(defaultTbl.width > 0 and defaultTbl.height > 0)
assert(defaultTbl.centerX and defaultTbl.centerY)
tbl.width = tbl.width or defaultTbl.width
tbl.height = tbl.height or defaultTbl.height
tbl.centerX = tbl.centerX or defaultTbl.centerX
tbl.centerY = tbl.centerY or defaultTbl.centerY
tbl.scale = tbl.scale or defaultTbl.scale
self._contextTable = tbl
self._defaultContextTable = defaultTbl
return self
end
--- Sets the context table from a settings object.
-- @tparam ApplicationFrame self The application frame object
-- @tparam Settings settings The settings object
-- @tparam string key The setting key
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.SetSettingsContext(self, settings, key)
return self:SetContextTable(settings[key], settings:GetDefaultReadOnly(key))
end
--- Sets the minimum size the application frame can be resized to.
-- @tparam ApplicationFrame self The application frame object
-- @tparam number minWidth The minimum width
-- @tparam number minHeight The minimum height
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.SetMinResize(self, minWidth, minHeight)
self._minWidth = minWidth
self._minHeight = minHeight
return self
end
--- Shows a dialog frame.
-- @tparam ApplicationFrame self The application frame object
-- @tparam Element frame The element to show in a dialog
-- @param context The context to set on the dialog frame
function ApplicationFrame.ShowDialogFrame(self, frame, context)
local dialogFrame = UIElements.New("Frame", "_dialog_"..random())
:SetRelativeLevel(DIALOG_RELATIVE_LEVEL * (#self._dialogStack + 1))
:SetBackgroundColor(Color.GetFullBlack():GetOpacity(DIALOG_OPACITY_PCT))
:AddAnchor("TOPLEFT")
:AddAnchor("BOTTOMRIGHT")
:SetMouseEnabled(true)
:SetMouseWheelEnabled(true)
:SetContext(context)
:SetScript("OnMouseWheel", NoOp)
:SetScript("OnMouseUp", private.DialogOnMouseUp)
:SetScript("OnHide", private.DialogOnHide)
:AddChildNoLayout(frame)
tinsert(self._dialogStack, dialogFrame)
self._contentFrame:AddChildNoLayout(dialogFrame)
dialogFrame:Show()
dialogFrame:Draw()
end
--- Show a confirmation dialog.
-- @tparam ApplicationFrame self The application frame object
-- @tparam string title The title of the dialog
-- @tparam string subTitle The sub-title of the dialog
-- @tparam function callback The callback for when the dialog is closed
-- @tparam[opt] varag ... Arguments to pass to the callback
function ApplicationFrame.ShowConfirmationDialog(self, title, subTitle, callback, ...)
local context = TempTable.Acquire(...)
context.callback = callback
local frame = UIElements.New("Frame", "frame")
:SetLayout("VERTICAL")
:SetSize(328, 158)
:SetPadding(12, 12, 8, 12)
:AddAnchor("CENTER")
:SetBackgroundColor("FRAME_BG", true)
:SetMouseEnabled(true)
:AddChild(UIElements.New("Frame", "header")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Text", "title")
:SetHeight(20)
:SetMargin(32, 8, 0, 0)
:SetFont("BODY_BODY2_BOLD")
:SetJustifyH("CENTER")
:SetText(title)
)
:AddChild(UIElements.New("Button", "closeBtn")
:SetBackgroundAndSize("iconPack.24x24/Close/Default")
:SetScript("OnClick", private.DialogCancelBtnOnClick)
)
)
:AddChild(UIElements.New("Text", "desc")
:SetMargin(0, 0, 16, 16)
:SetFont("BODY_BODY3")
:SetJustifyH("LEFT")
:SetJustifyV("TOP")
:SetText(subTitle)
)
:AddChild(UIElements.New("ActionButton", "confirmBtn")
:SetHeight(24)
:SetText(L["Confirm"])
:SetScript("OnClick", private.DialogConfirmBtnOnClick)
)
self:ShowDialogFrame(frame, context)
end
--- Show a dialog triggered by a "more" button.
-- @tparam ApplicationFrame self The application frame object
-- @tparam Button moreBtn The "more" button
-- @tparam function iter A dialog menu row iterator with the following fields: `index, text, callback`
function ApplicationFrame.ShowMoreButtonDialog(self, moreBtn, iter)
local frame = UIElements.New("PopupFrame", "moreDialog")
:SetLayout("VERTICAL")
:SetWidth(200)
:SetPadding(0, 0, 8, 4)
:AddAnchor("TOPRIGHT", moreBtn:_GetBaseFrame(), "BOTTOM", 22, -16)
local numRows = 0
for i, text, callback in iter do
frame:AddChild(UIElements.New("Button", "row"..i)
:SetHeight(20)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(text)
:SetScript("OnClick", callback)
)
numRows = numRows + 1
end
frame:SetHeight(12 + numRows * 20)
self:ShowDialogFrame(frame)
end
--- Show a menu dialog.
-- @tparam ApplicationFrame self The application frame object
-- @tparam string frame The frame to anchor the dialog to
-- @tparam function iter A menu row iterator with the following fields: `index, text, subIter`
-- @param context Context to pass to the iter / subIter
-- @tparam function clickCallback The function to be called when a menu row is clicked
-- @tparam boolean flip Flip the anchor to the other side
function ApplicationFrame.ShowMenuDialog(self, frame, iter, context, clickCallback, flip)
wipe(private.menuDialogContext)
private.menuDialogContext.context = context
private.menuDialogContext.clickCallback = clickCallback
self:ShowDialogFrame(private.CreateMenuDialogFrame("_menuDialog", iter)
:AddAnchor(flip and "TOPRIGHT" or "TOPLEFT", frame, flip and "BOTTOMRIGHT" or "BOTTOMLEFT", 2, -4)
)
end
--- Hides the current dialog.
-- @tparam ApplicationFrame self The application frame object
function ApplicationFrame.HideDialog(self)
local dialogFrame = tremove(self._dialogStack)
if not dialogFrame then
return
end
dialogFrame:GetParentElement():RemoveChild(dialogFrame)
dialogFrame:Hide()
dialogFrame:Release()
end
function ApplicationFrame.Draw(self)
local frame = self:_GetBaseFrame()
frame:SetToplevel(true)
frame:Raise()
self._nineSlice:SetVertexColor(Theme.GetColor("FRAME_BG"):GetFractionalRGBA())
-- update the size if it's less than the set min size
assert(self._minWidth > 0 and self._minHeight > 0)
self._contextTable.width = max(self._contextTable.width, self._minWidth)
self._contextTable.height = max(self._contextTable.height, self._minHeight)
self._contextTable.scale = max(self._contextTable.scale, MIN_SCALE)
-- set the frame size from the contextTable
self:SetScale(self._contextTable.scale)
self:SetSize(self._contextTable.width, self._contextTable.height)
-- make sure at least 50px of the frame is on the screen and offset by at least 1 scaled pixel to fix some rendering issues
local maxAbsCenterX = (UIParent:GetWidth() / self._contextTable.scale + self._contextTable.width) / 2 - MIN_ON_SCREEN_PX
local maxAbsCenterY = (UIParent:GetHeight() / self._contextTable.scale + self._contextTable.height) / 2 - MIN_ON_SCREEN_PX
local effectiveScale = UIParent:GetEffectiveScale()
if self._contextTable.centerX < 0 then
self._contextTable.centerX = min(max(self._contextTable.centerX, -maxAbsCenterX), -effectiveScale)
else
self._contextTable.centerX = max(min(self._contextTable.centerX, maxAbsCenterX), effectiveScale)
end
if self._contextTable.centerY < 0 then
self._contextTable.centerY = min(max(self._contextTable.centerY, -maxAbsCenterY), -effectiveScale)
else
self._contextTable.centerY = max(min(self._contextTable.centerY, maxAbsCenterY), effectiveScale)
end
-- adjust the position of the frame based on the UI scale to make rendering more consistent
self._contextTable.centerX = Math.Round(self._contextTable.centerX, effectiveScale)
self._contextTable.centerY = Math.Round(self._contextTable.centerY, effectiveScale)
-- set the frame position from the contextTable
self:WipeAnchors()
self:AddAnchor("CENTER", self._contextTable.centerX, self._contextTable.centerY)
self.__super:Draw()
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function ApplicationFrame._SavePositionAndSize(self, wasScaling)
local frame = self:_GetBaseFrame()
local parentFrame = frame:GetParent()
local width = frame:GetWidth()
local height = frame:GetHeight()
if wasScaling then
-- the anchor is in our old frame's scale, so convert the parent measurements to our old scale and then the resuslt to our new scale
local scaleAdjustment = width / self._contextTable.width
local frameLeftOffset = frame:GetLeft() - parentFrame:GetLeft() / self._contextTable.scale
self._contextTable.centerX = (frameLeftOffset - (parentFrame:GetWidth() / self._contextTable.scale - width) / 2) / scaleAdjustment
local frameBottomOffset = frame:GetBottom() - parentFrame:GetBottom() / self._contextTable.scale
self._contextTable.centerY = (frameBottomOffset - (parentFrame:GetHeight() / self._contextTable.scale - height) / 2) / scaleAdjustment
self._contextTable.scale = self._contextTable.scale * scaleAdjustment
else
self._contextTable.width = width
self._contextTable.height = height
-- the anchor is in our frame's scale, so convert the parent measurements to our scale
local frameLeftOffset = frame:GetLeft() - parentFrame:GetLeft() / self._contextTable.scale
self._contextTable.centerX = (frameLeftOffset - (parentFrame:GetWidth() / self._contextTable.scale - width) / 2)
local frameBottomOffset = frame:GetBottom() - parentFrame:GetBottom() / self._contextTable.scale
self._contextTable.centerY = (frameBottomOffset - (parentFrame:GetHeight() / self._contextTable.scale - height) / 2)
end
end
function ApplicationFrame._SetResizing(self, resizing)
if resizing then
self:GetElement("titleFrame"):Hide()
self._contentFrame:_GetBaseFrame():SetAlpha(0)
self._contentFrame:_GetBaseFrame():SetFrameStrata("LOW")
self._contentFrame:Draw()
else
self:GetElement("titleFrame"):Show()
self._contentFrame:_GetBaseFrame():SetAlpha(1)
self._contentFrame:_GetBaseFrame():SetFrameStrata(self._strata)
end
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.CloseButtonOnClick(button)
button:GetElement("__parent.__parent"):Hide()
end
function private.ResizeButtonOnEnter(self)
local tooltip = L["Click and drag to resize this window."].."\n"..
L["Hold SHIFT while dragging to scale the window instead."].."\n"..
L["Right-Click to reset the window size, scale, and position to their defaults."]
Tooltip.Show(self:_GetBaseFrame().resizeBtn, tooltip, true)
end
function private.ResizeButtonOnLeave(self)
Tooltip.Hide()
end
function private.ResizeButtonOnMouseDown(self, mouseButton)
if mouseButton ~= "LeftButton" then
return
end
self._isScaling = IsShiftKeyDown()
local frame = self:_GetBaseFrame()
local width = frame:GetWidth()
local height = frame:GetHeight()
if self._isScaling then
local minWidth = width * MIN_SCALE / self._contextTable.scale
local minHeight = height * MIN_SCALE / self._contextTable.scale
frame:SetMinResize(minWidth, minHeight)
frame:SetMaxResize(width * 10, height * 10)
else
frame:SetMinResize(self._minWidth, self._minHeight)
frame:SetMaxResize(width * 10, height * 10)
end
self:_SetResizing(true)
frame:StartSizing("BOTTOMRIGHT")
-- force updating the size here, to prevent using cached values from previously opened application frames
frame:SetWidth(width)
frame:SetHeight(height)
end
function private.ResizeButtonOnMouseUp(self, mouseButton)
if mouseButton ~= "LeftButton" then
return
end
self:_GetBaseFrame():StopMovingOrSizing()
self:_SetResizing(false)
self:_SavePositionAndSize(self._isScaling)
self._isScaling = nil
self:Draw()
end
function private.ResizeButtonOnClick(self, mouseButton)
if mouseButton ~= "RightButton" then
return
end
self._contextTable.scale = self._defaultContextTable.scale
self._contextTable.width = self._defaultContextTable.width
self._contextTable.height = self._defaultContextTable.height
self._contextTable.centerX = self._defaultContextTable.centerX
self._contextTable.centerY = self._defaultContextTable.centerY
self:Draw()
end
function private.FrameOnDragStart(self)
self:_GetBaseFrame():StartMoving()
end
function private.FrameOnDragStop(self)
self:_GetBaseFrame():StopMovingOrSizing()
self:_SavePositionAndSize()
self:Draw()
end
function private.DialogOnMouseUp(dialog)
local self = dialog:GetParentElement():GetParentElement()
self:HideDialog()
end
function private.DialogOnHide(dialog)
local context = dialog:GetContext()
if context then
TempTable.Release(context)
end
end
function private.DialogCancelBtnOnClick(button)
local self = button:GetBaseElement()
self:HideDialog()
end
function private.DialogConfirmBtnOnClick(button)
local self = button:GetBaseElement()
local dialogFrame = button:GetParentElement():GetParentElement()
local context = dialogFrame:GetContext()
dialogFrame:SetContext(nil)
self:HideDialog()
context.callback(TempTable.UnpackAndRelease(context))
end
function private.CreateMenuDialogFrame(id, iter)
local frame = UIElements.New("Frame", id)
:SetLayout("VERTICAL")
:SetWidth(180)
:SetPadding(2)
:SetBackgroundColor("PRIMARY_BG_ALT")
:SetBorderColor("ACTIVE_BG_ALT")
local numRows = 0
for i, text, subIter in iter, private.menuDialogContext.context do
frame:AddChild(UIElements.New("Frame", "row"..i)
:SetLayout("HORIZONTAL")
:SetHeight(21)
:SetContext(subIter)
:AddChild(UIElements.New("Button", "btn")
:SetHeight(21)
:SetPadding(8, 0, 0, 0)
:SetFont("BODY_BODY3_MEDIUM")
:SetBackground("PRIMARY_BG_ALT")
:SetHighlightEnabled(true)
:SetJustifyH("LEFT")
:SetIcon(subIter and "iconPack.12x12/Chevron/Right" or nil, subIter and "RIGHT" or nil)
:SetText(text)
:SetContext(i)
:SetScript("OnEnter", subIter and private.MenuDialogRowSubIterOnEnter or private.MenuDialogRowDefaultOnEnter)
:SetScript("OnClick", not subIter and private.MenuDialogRowOnClick or nil)
)
)
numRows = numRows + 1
end
frame:SetHeight(4 + numRows * 21)
return frame
end
function private.MenuDialogRowOnClick(button)
local path = TempTable.Acquire()
tinsert(path, button:GetContext())
local parentFrame = button:GetParentElement():GetParentElement():GetParentElement()
local self = parentFrame:GetBaseElement()
while parentFrame:GetParentElement() ~= self._contentFrame do
local selectedButton = parentFrame:GetContext():GetElement("btn")
tinsert(path, 1, selectedButton:GetContext())
parentFrame = parentFrame:GetParentElement()
end
private.menuDialogContext.clickCallback(button, private.menuDialogContext.context, TempTable.UnpackAndRelease(path))
end
function private.MenuDialogRowDefaultOnEnter(button)
local frame = button:GetParentElement():GetParentElement()
if frame:HasChildById("subFrame") then
local subFrame = frame:GetElement("subFrame")
local prevRow = frame:GetContext()
frame:SetContext(nil)
if prevRow then
prevRow:GetElement("btn"):SetHighlightLocked(false)
end
frame:RemoveChild(subFrame)
subFrame:Release()
frame:Draw()
end
end
function private.MenuDialogRowSubIterOnEnter(button)
private.MenuDialogRowDefaultOnEnter(button)
button:SetHighlightLocked(true)
local row = button:GetParentElement()
local frame = row:GetParentElement()
frame:SetContext(row)
local subFrame = private.CreateMenuDialogFrame("subFrame", row:GetContext())
:AddAnchor("TOPLEFT", button:_GetBaseFrame(), "TOPRIGHT", 4, 2)
frame:AddChildNoLayout(subFrame)
subFrame:Draw()
end
function private.GetAppStatusTooltip()
local tooltipLines = TempTable.Acquire()
tinsert(tooltipLines, format(L["TSM Desktop App Status (%s)"], TSM.GetRegion().."-"..GetRealmName()))
local appUpdateAge = time() - TSM.GetAppUpdateTime()
if appUpdateAge < 2 * SECONDS_PER_HOUR then
tinsert(tooltipLines, Theme.GetFeedbackColor("GREEN"):ColorText(format(L["App Synced %s Ago"], SecondsToTime(appUpdateAge))))
elseif appUpdateAge < 2 * SECONDS_PER_DAY then
tinsert(tooltipLines, Theme.GetFeedbackColor("YELLOW"):ColorText(format(L["App Synced %s Ago"], SecondsToTime(appUpdateAge))))
else
tinsert(tooltipLines, Theme.GetFeedbackColor("RED"):ColorText(L["App Not Synced"]))
end
local auctionDBRealmTime, auctionDBRegionTime = TSM.AuctionDB.GetAppDataUpdateTimes()
local auctionDBRealmAge = time() - auctionDBRealmTime
local auctionDBRegionAge = time() - auctionDBRegionTime
if auctionDBRealmAge < 4 * SECONDS_PER_HOUR then
tinsert(tooltipLines, Theme.GetFeedbackColor("GREEN"):ColorText(format(L["AuctionDB Realm Data is %s Old"], SecondsToTime(auctionDBRealmAge))))
elseif auctionDBRealmAge < 2 * SECONDS_PER_DAY then
tinsert(tooltipLines, Theme.GetFeedbackColor("YELLOW"):ColorText(format(L["AuctionDB Realm Data is %s Old"], SecondsToTime(auctionDBRealmAge))))
else
tinsert(tooltipLines, Theme.GetFeedbackColor("RED"):ColorText(L["No AuctionDB Realm Data"]))
end
if auctionDBRegionAge < 2 * SECONDS_PER_DAY then
tinsert(tooltipLines, Theme.GetFeedbackColor("GREEN"):ColorText(format(L["AuctionDB Region Data is %s Old"], SecondsToTime(auctionDBRegionAge))))
else
tinsert(tooltipLines, Theme.GetFeedbackColor("RED"):ColorText(L["No AuctionDB Region Data"]))
end
return strjoin("\n", TempTable.UnpackAndRelease(tooltipLines)), true, 16
end

View File

@@ -0,0 +1,189 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- ApplicationGroupTree UI Element Class.
-- An application group tree displays the group tree in a way which allows the user to select any number of them. This
-- element is used wherever the user needs to select groups to perform some action on. It is a subclass of the
-- @{GroupTree} class.
-- @classmod ApplicationGroupTree
local _, TSM = ...
local TempTable = TSM.Include("Util.TempTable")
local UIElements = TSM.Include("UI.UIElements")
local ApplicationGroupTree = TSM.Include("LibTSMClass").DefineClass("ApplicationGroupTree", TSM.UI.GroupTree)
UIElements.Register(ApplicationGroupTree)
TSM.UI.ApplicationGroupTree = ApplicationGroupTree
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ApplicationGroupTree.__init(self)
self.__super:__init()
self._selectedGroupsChangedHandler = nil
end
function ApplicationGroupTree.Release(self)
self._selectedGroupsChangedHandler = nil
self.__super:Release()
end
--- Registers a script handler.
-- @tparam ApplicationGroupTree self The application group tree object
-- @tparam string script The script to register for (supported scripts: `OnGroupSelectionChanged`)
-- @tparam function handler The script handler which will be called with the application group tree object followed by
-- any arguments to the script
-- @treturn ApplicationGroupTree The application group tree object
function ApplicationGroupTree.SetScript(self, script, handler)
if script == "OnGroupSelectionChanged" then
self._selectedGroupsChangedHandler = handler
else
error("Unknown ApplicationGroupTree script: "..tostring(script))
end
return self
end
--- Iterates through the selected groups.
-- @tparam ApplicationGroupTree self The application group tree object
-- @return Iterator with the following fields: `index, groupPath`
function ApplicationGroupTree.SelectedGroupsIterator(self)
local groups = TempTable.Acquire()
for _, groupPath in ipairs(self._allData) do
if self:_IsSelected(groupPath) then
tinsert(groups, groupPath)
end
end
return TempTable.Iterator(groups)
end
--- Sets the context table.
-- This table can be used to preserve selection state across lifecycles of the application group tree and even WoW
-- sessions if it's within the settings DB.
-- @see GroupTree.SetContextTable
-- @tparam ApplicationGroupTree self The application group tree object
-- @tparam table tbl The context table
-- @tparam table defaultTbl The default table (required fields: `unselected` OR `selected`, `collapsed`)
-- @treturn ApplicationGroupTree The application group tree object
function ApplicationGroupTree.SetContextTable(self, tbl, defaultTbl)
if defaultTbl.unselected then
assert(type(defaultTbl.unselected) == "table" and not defaultTbl.selected)
tbl.unselected = tbl.unselected or CopyTable(defaultTbl.unselected)
tbl.selected = nil
else
assert(type(defaultTbl.selected) == "table" and not defaultTbl.unselected)
tbl.selected = tbl.selected or CopyTable(defaultTbl.selected)
tbl.unselected = nil
end
self.__super:SetContextTable(tbl, defaultTbl)
return self
end
--- Gets whether or not a group is currently selected.
-- @tparam ApplicationGroupTree self The application group tree object
-- @tparam string groupPath The group to check
-- @treturn boolean Whether or not the group is selected
function ApplicationGroupTree.IsGroupSelected(self, groupPath)
return self:_IsSelected(groupPath)
end
--- Gets whether or not a group is currently selected.
-- @tparam ApplicationGroupTree self The application group tree object
-- @tparam string groupPath The group to set the selected state of
-- @tparam boolean selected Whether or not the group should be selected
-- @treturn ApplicationGroupTree The application group tree object
function ApplicationGroupTree.SetGroupSelected(self, groupPath, selected)
self:_SetSelected(groupPath, selected)
return self
end
--- Gets whether or not the selection is cleared.
-- @tparam ApplicationGroupTree self The application group tree object
-- @tparam[opt=false] boolean updateData Whether or not to update the data first
-- @treturn boolean Whether or not the selection is cleared
function ApplicationGroupTree.IsSelectionCleared(self, updateData)
if updateData then
self:_UpdateData()
end
for _, groupPath in ipairs(self._searchStr == "" and self._allData or self._data) do
if self:_IsSelected(groupPath) then
return false
end
end
return true
end
--- Toggle the selection state of the application group tree.
-- @tparam ApplicationGroupTree self The application group tree object
-- @treturn ApplicationGroupTree The application group tree object
function ApplicationGroupTree.ToggleSelectAll(self)
local isCleared = self:IsSelectionCleared()
for _, groupPath in ipairs(self._searchStr == "" and self._allData or self._data) do
self:_SetSelected(groupPath, isCleared)
end
self:Draw()
if self._selectedGroupsChangedHandler then
self:_selectedGroupsChangedHandler()
end
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function ApplicationGroupTree._UpdateData(self)
self.__super:_UpdateData()
-- remove data which is no longer present from _contextTable
local selectedGroups = TempTable.Acquire()
for _, groupPath in ipairs(self._allData) do
if self:_IsSelected(groupPath) then
selectedGroups[groupPath] = true
end
end
wipe(self._contextTable.selected or self._contextTable.unselected)
for _, groupPath in ipairs(self._allData) do
self:_SetSelected(groupPath, selectedGroups[groupPath])
end
TempTable.Release(selectedGroups)
end
function ApplicationGroupTree._IsSelected(self, data)
if self._contextTable.unselected then
return not self._contextTable.unselected[data]
else
return self._contextTable.selected[data]
end
end
function ApplicationGroupTree._SetSelected(self, data, selected)
if self._contextTable.unselected then
self._contextTable.unselected[data] = not selected or nil
else
self._contextTable.selected[data] = selected or nil
end
end
function ApplicationGroupTree._HandleRowClick(self, data, mouseButton)
if mouseButton == "RightButton" then
self.__super:_HandleRowClick(data, mouseButton)
return
end
self:_SetSelected(data, not self:_IsSelected(data))
-- also set the selection for all child groups to the same as this group
for _, groupPath in ipairs(self._allData) do
if TSM.Groups.Path.IsChild(groupPath, data) and data ~= TSM.CONST.ROOT_GROUP_PATH then
self:_SetSelected(groupPath, self:_IsSelected(data))
end
end
self:Draw()
if self._selectedGroupsChangedHandler then
self:_selectedGroupsChangedHandler()
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,245 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Base Dropdown UI Element Class.
-- The base dropdown class is an abstract class which provides shared functionality between the @{SelectionDropdown} and
-- @{MultiselectionDropdown} classes. It is a subclass of the @{Text} class.
-- @classmod BaseDropdown
local _, TSM = ...
local NineSlice = TSM.Include("Util.NineSlice")
local Color = TSM.Include("Util.Color")
local Theme = TSM.Include("Util.Theme")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local BaseDropdown = TSM.Include("LibTSMClass").DefineClass("BaseDropdown", TSM.UI.Text, "ABSTRACT")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(BaseDropdown)
TSM.UI.BaseDropdown = BaseDropdown
local private = {}
local EXPANDER_SIZE = 18
local TEXT_PADDING = 8
local EXPANDER_PADDING = 8
-- ============================================================================
-- Meta Class Methods
-- ============================================================================
function BaseDropdown.__init(self)
local frame = UIElements.CreateFrame(self, "Button", nil, nil, nil)
self.__super:__init(frame)
self._nineSlice = NineSlice.New(frame)
ScriptWrapper.Set(frame, "OnClick", private.FrameOnClick, self)
frame.arrow = frame:CreateTexture(nil, "ARTWORK")
self._widthText = UIElements.CreateFontString(self, frame)
self._widthText:Hide()
self._font = "BODY_BODY2"
self._hintText = ""
self._items = {}
self._itemKeyLookup = {}
self._disabled = false
self._isOpen = false
self._onSelectionChangedHandler = nil
end
function BaseDropdown.Release(self)
self._hintText = ""
wipe(self._items)
wipe(self._itemKeyLookup)
self._disabled = false
self._isOpen = false
self._onSelectionChangedHandler = nil
self:_GetBaseFrame():Enable()
self.__super:Release()
self._font = "BODY_BODY2"
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
--- Sets the hint text which is shown when there's no selection.
-- @tparam BaseDropdown self The dropdown object
-- @tparam string text The hint text string
-- @treturn BaseDropdown The dropdown object
function BaseDropdown.SetHintText(self, text)
self._hintText = text
return self
end
--- Add an item to be shown in the dropdown dialog list.
-- @tparam BaseDropdown self The dropdown object
-- @tparam string item The item to add to the list (localized string)
-- @tparam[opt] string|number itemKey The internal representation of the item (if not specified will be the index)
-- @treturn BaseDropdown The dropdown object
function BaseDropdown.AddItem(self, item, itemKey)
tinsert(self._items, item)
self._itemKeyLookup[item] = itemKey or #self._items
return self
end
--- Set the items to show in the dropdown dialog list.
-- @tparam BaseDropdown self The dropdown object
-- @tparam table items A list of items to be shown in the dropdown list
-- @tparam[opt] table itemKeys A list of keys which go with the item at the corresponding index in the items list
-- @treturn BaseDropdown The dropdown object
function BaseDropdown.SetItems(self, items, itemKeys)
wipe(self._items)
wipe(self._itemKeyLookup)
assert(not itemKeys or #itemKeys == #items)
for i, item in ipairs(items) do
self:AddItem(item, itemKeys and itemKeys[i])
end
return self
end
--- Set whether or not the dropdown is disabled.
-- @tparam BaseDropdown self The dropdown object
-- @tparam boolean disabled Whether or not to disable the dropdown
-- @treturn BaseDropdown The dropdown object
function BaseDropdown.SetDisabled(self, disabled)
self._disabled = disabled
if disabled then
self:_GetBaseFrame():Disable()
else
self:_GetBaseFrame():Enable()
end
return self
end
--- Registers a script handler.
-- @tparam BaseDropdown self The dropdown object
-- @tparam string script The script to register for (supported scripts: `OnSelectionChanged`)
-- @tparam function handler The script handler which will be called with the dropdown object followed by any arguments
-- to the script
-- @treturn BaseDropdown The dropdown object
function BaseDropdown.SetScript(self, script, handler)
if script == "OnSelectionChanged" then
self._onSelectionChangedHandler = handler
else
error("Invalid BaseDropdown script: "..tostring(script))
end
return self
end
--- Sets whether or not the dropdown is open.
-- @tparam BaseDropdown self The dropdown object
-- @tparam boolean open Whether or not the dropdown is open
-- @treturn BaseDropdown The dropdown object
function BaseDropdown.SetOpen(self, open)
assert(type(open) == "boolean")
if open == self._isOpen then
return self
end
self._isOpen = open
if open then
local width, height = self:_GetDialogSize()
local dialogFrame = UIElements.New("Frame", "dropdown")
:SetLayout("VERTICAL")
:SetContext(self)
:AddAnchor("TOPLEFT", self:_GetBaseFrame(), "BOTTOMLEFT", 0, -4)
:SetPadding(0, 0, 4, 4)
:SetBackgroundColor("ACTIVE_BG", true)
:SetSize(max(width, self:_GetDimension("WIDTH")), height)
:SetScript("OnHide", private.DialogOnHide)
self:_AddDialogChildren(dialogFrame)
dialogFrame:GetElement("list"):SetScript("OnSelectionChanged", private.ListOnSelectionChanged)
self:GetBaseElement():ShowDialogFrame(dialogFrame)
else
self:GetBaseElement():HideDialog()
end
return self
end
function BaseDropdown.SetText(self)
error("BaseDropdown does not support this method")
end
function BaseDropdown.SetTextColor(self, color)
error("BaseDropdown does not support this method")
end
function BaseDropdown.Draw(self)
self.__super:SetText(self:_GetCurrentSelectionString())
self.__super:Draw()
local frame = self:_GetBaseFrame()
TSM.UI.TexturePacks.SetTexture(frame.arrow, "iconPack.18x18/Chevron/Down")
local frameHeight = frame:GetHeight()
local paddingX = EXPANDER_PADDING
local paddingY = (frameHeight - EXPANDER_SIZE) / 2
frame.text:ClearAllPoints()
frame.text:SetPoint("TOPLEFT", TEXT_PADDING, 0)
frame.text:SetPoint("BOTTOMRIGHT", -EXPANDER_SIZE, 0)
frame.arrow:ClearAllPoints()
frame.arrow:SetPoint("BOTTOMLEFT", frame.text, "BOTTOMRIGHT", -paddingX, paddingY)
frame.arrow:SetPoint("TOPRIGHT", -paddingX, -paddingY)
-- set textures and text color depending on the state
self._nineSlice:SetStyle("rounded")
local textColor = self:_GetTextColor()
frame.text:SetTextColor(textColor:GetFractionalRGBA())
self._nineSlice:SetVertexColor(Theme.GetColor(self._disabled and "PRIMARY_BG_ALT" or "ACTIVE_BG"):GetFractionalRGBA())
frame.arrow:SetVertexColor(textColor:GetFractionalRGBA())
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function BaseDropdown._GetTextColor(self)
local color = Theme.GetColor(self._disabled and "PRIMARY_BG_ALT" or "ACTIVE_BG")
-- the text color should have maximum contrast with the dropdown color, so set it to white/black based on the dropdown color
if color:IsLight() then
-- the dropdown is light, so set the text to black
return Color.GetFullBlack():GetTint(self._disabled and "-DISABLED" or 0)
else
-- the dropdown is dark, so set the text to white
return Color.GetFullWhite():GetTint(self._disabled and "+DISABLED" or 0)
end
end
function BaseDropdown._GetDialogSize(self)
local maxStringWidth = 100 -- no smaller than 100
self._widthText:Show()
self._widthText:SetFont(Theme.GetFont(self._font):GetWowFont())
for _, item in ipairs(self._items) do
self._widthText:SetText(item)
maxStringWidth = max(maxStringWidth, self._widthText:GetUnboundedStringWidth())
end
self._widthText:Hide()
return maxStringWidth + Theme.GetColSpacing() * 2, 8 + max(16, min(8, #self._items) * 20)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.FrameOnClick(self)
self:SetOpen(true)
end
function private.ListOnSelectionChanged(dropdownList, selection)
local self = dropdownList:GetParentElement():GetContext()
self:_OnListSelectionChanged(dropdownList, selection)
self:Draw()
end
function private.DialogOnHide(frame)
local self = frame:GetContext()
self._isOpen = false
end

View File

@@ -0,0 +1,584 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Base Input UI Element Class.
-- The base input class is an abstract class which provides shared functionality between the @{Input} and
-- @{MultiLineInput} classes. It is a subclass of the @{Element} class.
-- @classmod BaseInput
local _, TSM = ...
local L = TSM.Include("Locale").GetTable()
local NineSlice = TSM.Include("Util.NineSlice")
local Log = TSM.Include("Util.Log")
local Color = TSM.Include("Util.Color")
local Theme = TSM.Include("Util.Theme")
local Delay = TSM.Include("Util.Delay")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local CustomPrice = TSM.Include("Service.CustomPrice")
local BaseInput = TSM.Include("LibTSMClass").DefineClass("BaseInput", TSM.UI.Element, "ABSTRACT")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(BaseInput)
TSM.UI.BaseInput = BaseInput
local private = {}
local BORDER_THICKNESS = 1
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function BaseInput.__init(self, frame)
self.__super:__init(frame)
self._borderNineSlice = NineSlice.New(frame)
self._borderNineSlice:Hide()
self._backgroundNineSlice = NineSlice.New(frame, 1)
self._backgroundNineSlice:Hide()
self._editBox:SetShadowColor(0, 0, 0, 0)
self._editBox:SetAutoFocus(false)
ScriptWrapper.Set(self._editBox, "OnEscapePressed", private.OnEscapePressed, self)
ScriptWrapper.Set(self._editBox, "OnTabPressed", private.OnTabPressed, self)
ScriptWrapper.Set(self._editBox, "OnEditFocusGained", private.OnEditFocusGained, self)
ScriptWrapper.Set(self._editBox, "OnEditFocusLost", private.OnEditFocusLost, self)
ScriptWrapper.Set(self._editBox, "OnChar", self._OnChar, self)
self._lostFocusDelayName = "INPUT_LOST_FOCUS_"..tostring(frame)
self._backgroundColor = "ACTIVE_BG_ALT"
self._borderColor = nil
self._value = ""
self._escValue = nil
self._justifyH = "LEFT"
self._justifyV = "MIDDLE"
self._font = "BODY_BODY2"
self._pasteMode = nil
self._validateFunc = nil
self._validateContext = nil
self._settingTable = nil
self._settingKey = nil
self._disabled = false
self._isValid = true
self._tabPrevPath = nil
self._tabNextPath = nil
self._onValueChangedHandler = nil
self._onEnterPressedHandler = nil
self._onValidationChangedHandler = nil
self._onFocusLostHandler = nil
self._pasteChars = {}
end
function BaseInput.Acquire(self)
self.__super:Acquire()
ScriptWrapper.Set(self._editBox, "OnEnterPressed", private.OnEnterPressed, self)
ScriptWrapper.Set(self._editBox, "OnTextChanged", private.OnTextChanged, self)
end
function BaseInput.Release(self)
Delay.Cancel(self._lostFocusDelayName)
ScriptWrapper.Clear(self._editBox, "OnEnterPressed")
ScriptWrapper.Clear(self._editBox, "OnTextChanged")
self._editBox:SetText("")
self._editBox:ClearFocus()
self._editBox:Enable()
self._editBox:EnableMouse(true)
self._editBox:EnableKeyboard(true)
self._editBox:HighlightText(0, 0)
self._editBox:SetHitRectInsets(0, 0, 0, 0)
self._editBox:SetMaxLetters(2147483647)
self._editBox:SetMaxBytes(2147483647)
self._backgroundColor = "ACTIVE_BG_ALT"
self._borderColor = nil
self._value = ""
self._escValue = nil
self._justifyH = "LEFT"
self._justifyV = "MIDDLE"
self._font = "BODY_BODY2"
self._pasteMode = nil
self._validateFunc = nil
self._validateContext = nil
self._settingTable = nil
self._settingKey = nil
self._disabled = false
self._isValid = true
self._tabPrevPath = nil
self._tabNextPath = nil
self._onValueChangedHandler = nil
self._onEnterPressedHandler = nil
self._onValidationChangedHandler = nil
self._onFocusLostHandler = nil
wipe(self._pasteChars)
self.__super:Release()
end
--- Sets the background of the input.
-- @tparam BaseInput self The input object
-- @tparam ?string|nil color The background color as a theme color key or nil
-- @treturn BaseInput The input object
function BaseInput.SetBackgroundColor(self, color)
assert(color == nil or Theme.GetColor(color))
self._backgroundColor = color
return self
end
--- Sets the border of the input.
-- @tparam BaseInput self The input object
-- @tparam ?string|nil color The border color as a theme color key or nil
-- @treturn BaseInput The input object
function BaseInput.SetBorderColor(self, color)
assert(color == nil or Theme.GetColor(color))
self._borderColor = color
return self
end
--- Sets the horizontal justification of the input.
-- @tparam BaseInput self The input object
-- @tparam string justifyH The horizontal justification (either "LEFT", "CENTER" or "RIGHT")
-- @treturn BaseInput The input object
function BaseInput.SetJustifyH(self, justifyH)
assert(justifyH == "LEFT" or justifyH == "CENTER" or justifyH == "RIGHT")
self._justifyH = justifyH
return self
end
--- Sets the vertical justification of the input.
-- @tparam BaseInput self The input object
-- @tparam string justifyV The vertical justification (either "TOP", "MIDDLE" or "BOTTOM")
-- @treturn BaseInput The input object
function BaseInput.SetJustifyV(self, justifyV)
assert(justifyV == "TOP" or justifyV == "MIDDLE" or justifyV == "BOTTOM")
self._justifyV = justifyV
return self
end
--- Sets the font.
-- @tparam BaseInput self The input object
-- @tparam string font The font key
-- @treturn BaseInput The input object
function BaseInput.SetFont(self, font)
assert(Theme.GetFont(font))
self._font = font
return self
end
--- Sets the path of the inputs to jump to when tab (or shift-tab to go backwards) is pressed.
-- @tparam BaseInput self The input object
-- @tparam string prevPath The path to the previous input (for shift-tab)
-- @tparam string nextPath The path to the next input (for tab)
-- @treturn BaseInput The input object
function BaseInput.SetTabPaths(self, prevPath, nextPath)
self._tabPrevPath = prevPath
self._tabNextPath = nextPath
return self
end
--- Set the highlight to all or some of the input's text.
-- @tparam BaseInput self The input object
-- @tparam number starting The position at which to start the highlight
-- @tparam number ending The position at which to stop the highlight
-- @treturn BaseInput The input object
function BaseInput.HighlightText(self, starting, ending)
if starting and ending then
self._editBox:HighlightText(starting, ending)
else
self._editBox:HighlightText()
end
return self
end
--- Sets the current value.
-- @tparam BaseInput self The input object
-- @tparam string value The value
-- @treturn BaseInput The input object
function BaseInput.SetValue(self, value)
if type(value) == "number" then
value = tostring(value)
end
assert(type(value) == "string")
if self:_SetValueHelper(value, true) then
self._escValue = self._value
else
self._escValue = nil
end
return self
end
--- Sets whether or not the input is disabled.
-- @tparam BaseInput self The input object
-- @tparam boolean disabled Whether or not the input is disabled
-- @treturn BaseInput The input object
function BaseInput.SetDisabled(self, disabled)
self._disabled = disabled
if disabled then
self._editBox:Disable()
else
self._editBox:Enable()
end
return self
end
--- Sets the function to use to validate the input text.
-- @tparam BaseInput self The input object
-- @tparam ?string|function validateFunc A function which returns true if the passed text is valid
-- or false and an error message if not, or one of the following strings for built in validate
-- functions: "CUSTOM_PRICE"
-- @param[opt=nil] context Extra context to pass to the validate function. For the built-in
-- "CUSTOM_PRICE" function, this is optionally a list of bad sources. For the built-in "NUMBER"
-- function, this must be a string such as "0:1000" to specify the min and max values.
-- @treturn BaseInput The input object
function BaseInput.SetValidateFunc(self, validateFunc, context)
if type(validateFunc) == "function" then
self._validateFunc = validateFunc
self._validateContext = context
elseif validateFunc == "CUSTOM_PRICE" then
assert(context == nil or type(context) == "table")
self._validateFunc = private.CustomPriceValidateFunc
self._validateContext = context
elseif validateFunc == "NUMBER" then
local minVal, maxVal, extra = strsplit(":", context)
assert(tonumber(minVal) <= tonumber(maxVal) and not extra)
self._validateFunc = private.NumberValidateFunc
self._validateContext = context
else
error("Invalid validateFunc: "..tostring(validateFunc))
end
return self
end
--- Returns the input's focus state.
-- @tparam BaseInput self The input object
function BaseInput.HasFocus(self)
return self._editBox:HasFocus()
end
--- Sets whether or not this input is focused.
-- @tparam BaseInput self The input object
-- @tparam boolean focused Whether or not this input is focused
-- @treturn BaseInput The input object
function BaseInput.SetFocused(self, focused)
if focused then
self._editBox:SetFocus()
else
self._editBox:ClearFocus()
end
return self
end
--- Clears the highlight.
-- @tparam BaseInput self The input object
-- @treturn BaseInput The input object
function BaseInput.ClearHighlight(self)
self._editBox:HighlightText(0, 0)
return self
end
--- Set the maximum number of letters for the input's entered text.
-- @tparam BaseInput self The input object
-- @tparam number number The number of letters for entered text
-- @treturn BaseInput The input object
function BaseInput.SetMaxLetters(self, number)
self._editBox:SetMaxLetters(number)
return self
end
--- Gets the input value.
-- @tparam BaseInput self The input object
-- @treturn string The input value
function BaseInput.GetValue(self)
return self._ignoreEnter and self._value or strtrim(self._value)
end
--- Registers a script handler.
-- @tparam BaseInput self The input object
-- @tparam string script The script to register for
-- @tparam[opt=nil] function handler The script handler which should be called
-- @treturn BaseInput The element object
function BaseInput.SetScript(self, script, handler)
if script == "OnValueChanged" then
self._onValueChangedHandler = handler
elseif script == "OnEnterPressed" then
self._onEnterPressedHandler = handler
elseif script == "OnValidationChanged" then
self._onValidationChangedHandler = handler
elseif script == "OnFocusLost" then
self._onFocusLostHandler = handler
else
error("Invalid base input script: "..tostring(script))
end
return self
end
--- Sets the setting info.
-- This method is used to have the value of the input automatically correspond with the value of a field in a table.
-- This is useful for inputs which are tied directly to settings.
-- @tparam BaseInput self The input object
-- @tparam table tbl The table which the field to set belongs to
-- @tparam string key The key into the table to be set based on the input state
-- @treturn BaseInput The input object
function BaseInput.SetSettingInfo(self, tbl, key)
assert(self._value == "")
self._settingTable = tbl
self._settingKey = key
self:SetValue(tbl[key])
return self
end
--- Get the current validation state.
-- @tparam BaseInput self The input object
-- @treturn boolean The current valiation state
function BaseInput.IsValid(self)
return self._isValid
end
--- Sets the input into paste mode for supporting the pasting of large strings.
-- @tparam BaseInput self The input object
-- @treturn BaseInput The input object
function BaseInput.SetPasteMode(self)
self._pasteMode = true
ScriptWrapper.Clear(self._editBox, "OnTextChanged")
self._editBox:SetMaxBytes(1)
return self
end
function BaseInput.Draw(self)
self.__super:Draw()
self:_DrawBackgroundAndBorder()
-- set the font
self._editBox:SetFont(Theme.GetFont(self._font):GetWowFont())
-- set the justification
self._editBox:SetJustifyH(self._justifyH)
self._editBox:SetJustifyV(self._justifyV)
-- set the text color
self._editBox:SetTextColor(self:_GetTextColor():GetFractionalRGBA())
-- set the highlight color
self._editBox:SetHighlightColor(Theme.GetColor("TEXT%HIGHLIGHT"):GetFractionalRGBA())
if not self._editBox:HasFocus() then
-- set the text
self._editBox:SetText(self._value)
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function BaseInput._GetTextColor(self, tint)
local color = Theme.GetColor(self._disabled and "PRIMARY_BG_ALT" or self._backgroundColor)
-- the text color should have maximum contrast with the input color, so set it to white/black based on the input color
if color:IsLight() then
-- the input is light, so set the text to black
return Color.GetFullBlack():GetTint(self._disabled and "-DISABLED" or tint or 0)
else
-- the input is dark, so set the text to white
return Color.GetFullWhite():GetTint(self._disabled and "+DISABLED" or tint or 0)
end
end
function BaseInput._SetValueHelper(self, value, noCallback)
if not self._validateFunc or self:_validateFunc(strtrim(value), self._validateContext) then
self._value = value
if self._settingTable then
if type(self._settingTable[self._settingKey]) == "number" then
value = tonumber(value)
assert(value)
end
self._settingTable[self._settingKey] = value
end
if not noCallback and self._onValueChangedHandler then
self:_onValueChangedHandler()
end
if not self._isValid then
self._isValid = true
self:_DrawBackgroundAndBorder()
if self._onValidationChangedHandler then
self:_onValidationChangedHandler()
end
end
return true
else
if self._isValid then
self._isValid = false
self:_DrawBackgroundAndBorder()
if self._onValidationChangedHandler then
self:_onValidationChangedHandler()
end
end
return false
end
end
function BaseInput._DrawBackgroundAndBorder(self)
assert(self._backgroundColor)
self._backgroundNineSlice:SetStyle("rounded", (self._borderColor or not self._isValid) and BORDER_THICKNESS or nil)
self._backgroundNineSlice:SetVertexColor(Theme.GetColor(self._disabled and "PRIMARY_BG_ALT" or self._backgroundColor):GetFractionalRGBA())
if self._borderColor or not self._isValid then
self._borderNineSlice:SetStyle("rounded")
self._borderNineSlice:SetVertexColor((not self._isValid and Theme.GetFeedbackColor("RED") or Theme.GetColor(self._borderColor)):GetFractionalRGBA())
else
self._borderNineSlice:Hide()
end
end
function BaseInput._OnChar(self, c)
-- can be overridden
if not self._pasteMode then
return
end
tinsert(self._pasteChars, c)
ScriptWrapper.Set(self._editBox, "OnUpdate", private.OnUpdate, self)
end
function BaseInput._OnTextChanged(self, value)
-- can be overridden
end
function BaseInput._ShouldKeepFocus(self)
-- can be overridden
return false
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnEscapePressed(self)
if self._escValue then
self._value = self._escValue
assert(self:_SetValueHelper(self._escValue))
end
self:SetFocused(false)
self:HighlightText(0, 0)
self:Draw()
end
function private.OnTabPressed(self)
local isValid, err = true, nil
if self._validateFunc then
local value = strtrim(self._editBox:GetText())
isValid, err = self:_validateFunc(value, self._validateContext)
end
if not isValid and err then
-- TODO: better way to show the error message?
Log.PrintUser(err)
end
self:SetFocused(false)
self:HighlightText(0, 0)
if self._tabPrevPath and IsShiftKeyDown() then
self:GetElement(self._tabPrevPath):SetFocused(true)
elseif self._tabNextPath and not IsShiftKeyDown() then
self:GetElement(self._tabNextPath):SetFocused(true)
end
end
function private.OnEnterPressed(self)
local isValid, err = true, nil
if self._validateFunc then
local value = strtrim(self._editBox:GetText())
isValid, err = self:_validateFunc(value, self._validateContext)
end
if not isValid and err then
-- TODO: better way to show the error message?
Log.PrintUser(err)
end
if isValid then
self:SetFocused(false)
self:HighlightText(0, 0)
if self._onEnterPressedHandler then
self:_onEnterPressedHandler()
end
end
end
function private.OnEditFocusGained(self)
Delay.Cancel(self._lostFocusDelayName)
self:Draw()
self:HighlightText()
end
function private.OnEditFocusLost(self)
if self:_ShouldKeepFocus() then
self:SetFocused(true)
return
end
if self._isValid then
self._escValue = self._value
end
self:HighlightText(0, 0)
self:Draw()
if not self._isValid then
self._isValid = true
self:_DrawBackgroundAndBorder()
if self._onValidationChangedHandler then
self:_onValidationChangedHandler()
end
end
-- wait until the next frame before calling the handler
Delay.AfterFrame(self._lostFocusDelayName, 0, private.OnFocusLost, nil, self)
end
function private.OnFocusLost(self)
if self:HasFocus() then
return
end
if self._onFocusLostHandler then
self:_onFocusLostHandler()
end
end
function private.OnTextChanged(self, isUserInput)
if not isUserInput then
return
end
local value = self._editBox:GetText()
self:_SetValueHelper(value)
self:_OnTextChanged(value)
end
function private.OnUpdate(self)
ScriptWrapper.Clear(self._editBox, "OnUpdate")
local value = table.concat(self._pasteChars)
wipe(self._pasteChars)
self:_SetValueHelper(value)
self:_OnTextChanged(value)
end
-- ============================================================================
-- Built In Validate Functions
-- ============================================================================
function private.CustomPriceValidateFunc(_, value, badSources)
local isValid, err = CustomPrice.Validate(value, badSources)
if not isValid then
return false, L["Invalid custom price."].." "..err
end
return true
end
function private.NumberValidateFunc(input, value, range)
local minValue, maxValue = strsplit(":", range)
minValue = tonumber(minValue)
maxValue = tonumber(maxValue)
value = tonumber(value)
if not value then
return false, L["Invalid numeric value."]
elseif value < minValue or value > maxValue then
return false, format(L["Value must be between %d and %d."], minValue, maxValue)
end
return true
end

285
Core/UI/Elements/Button.lua Normal file
View File

@@ -0,0 +1,285 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Button UI Element Class.
-- A button is a clickable element which has text drawn over top of it. It is a subclass of the @{Text} class.
-- @classmod Button
local _, TSM = ...
local Button = TSM.Include("LibTSMClass").DefineClass("Button", TSM.UI.Text)
local Theme = TSM.Include("Util.Theme")
local ItemInfo = TSM.Include("Service.ItemInfo")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Button)
TSM.UI.Button = Button
local ICON_SPACING = 4
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Button.__init(self)
local frame = UIElements.CreateFrame(self, "Button")
self.__super:__init(frame)
frame.backgroundTexture = frame:CreateTexture(nil, "BACKGROUND")
-- create the highlight
frame.highlight = frame:CreateTexture(nil, "HIGHLIGHT")
frame.highlight:SetAllPoints()
frame.highlight:SetBlendMode("BLEND")
frame:SetHighlightTexture(frame.highlight)
-- create the icon
frame.icon = frame:CreateTexture(nil, "ARTWORK")
self._font = "BODY_BODY1"
self._justifyH = "CENTER"
self._background = nil
self._iconTexturePack = nil
self._iconPosition = nil
self._highlightEnabled = false
end
function Button.Acquire(self)
self:_GetBaseFrame():Enable()
self:_GetBaseFrame():RegisterForClicks("LeftButtonUp")
self:_GetBaseFrame():SetHitRectInsets(0, 0, 0, 0)
self.__super:Acquire()
end
function Button.Release(self)
local frame = self:_GetBaseFrame()
frame:UnlockHighlight()
self._background = nil
self._iconTexturePack = nil
self._iconPosition = nil
self._highlightEnabled = false
self.__super:Release()
self._font = "BODY_BODY1"
self._justifyH = "CENTER"
end
--- Sets the background of the button.
-- @tparam Button self The button object
-- @tparam ?string|number|nil background Either a texture pack string, itemString, WoW file id, theme color key, or nil
-- @treturn Button The button object
function Button.SetBackground(self, background)
assert(background == nil or type(background) == "string" or type(background) == "number")
self._background = background
return self
end
--- Sets the background and size of the button based on a texture pack string.
-- @tparam Button self The button object
-- @tparam string texturePack A texture pack string to set the background to and base the size on
-- @treturn Button The button object
function Button.SetBackgroundAndSize(self, texturePack)
self:SetBackground(texturePack)
self:SetSize(TSM.UI.TexturePacks.GetSize(texturePack))
return self
end
--- Sets whether or not the highlight is enabled.
-- @tparam Button self The button object
-- @tparam boolean enabled Whether or not the highlight is enabled
-- @treturn Button The button object
function Button.SetHighlightEnabled(self, enabled)
self._highlightEnabled = enabled
return self
end
--- Sets the icon that shows within the button.
-- @tparam Button self The button object
-- @tparam[opt=nil] string texturePack A texture pack string to set the icon and its size to
-- @tparam[opt=nil] string position The positin of the icon
-- @treturn Button The button object
function Button.SetIcon(self, texturePack, position)
if texturePack or position then
assert(TSM.UI.TexturePacks.IsValid(texturePack))
assert(position == "LEFT" or position == "LEFT_NO_TEXT" or position == "CENTER" or position == "RIGHT")
self._iconTexturePack = texturePack
self._iconPosition = position
else
self._iconTexturePack = nil
self._iconPosition = nil
end
return self
end
--- Set whether or not the button is disabled.
-- @tparam Button self The button object
-- @tparam boolean disabled Whether or not the button should be disabled
-- @treturn Button The button object
function Button.SetDisabled(self, disabled)
if disabled then
self:_GetBaseFrame():Disable()
else
self:_GetBaseFrame():Enable()
end
return self
end
--- Registers the button for drag events.
-- @tparam Button self The button object
-- @tparam string button The mouse button to register for drag events from
-- @treturn Button The button object
function Button.RegisterForDrag(self, button)
self:_GetBaseFrame():RegisterForDrag(button)
return self
end
--- Click on the button.
-- @tparam Button self The button object
function Button.Click(self)
self:_GetBaseFrame():Click()
end
--- Enable right-click events for the button.
-- @tparam Button self The button object
-- @treturn Button The button object
function Button.EnableRightClick(self)
self:_GetBaseFrame():RegisterForClicks("LeftButtonUp", "RightButtonUp")
return self
end
--- Set the hit rectangle insets for the button.
-- @tparam Button self The button object
-- @tparam number left How much the left side of the hit rectangle is inset
-- @tparam number right How much the right side of the hit rectangle is inset
-- @tparam number top How much the top side of the hit rectangle is inset
-- @tparam number bottom How much the bottom side of the hit rectangle is inset
-- @treturn Button The button object
function Button.SetHitRectInsets(self, left, right, top, bottom)
self:_GetBaseFrame():SetHitRectInsets(left, right, top, bottom)
return self
end
--- Set whether or not to lock the button's highlight.
-- @tparam Button self The action button object
-- @tparam boolean locked Whether or not to lock the action button's highlight
-- @treturn Button The action button object
function Button.SetHighlightLocked(self, locked)
if locked then
self:_GetBaseFrame():LockHighlight()
else
self:_GetBaseFrame():UnlockHighlight()
end
return self
end
function Button.Draw(self)
local frame = self:_GetBaseFrame()
frame.text:Show()
self.__super:Draw()
frame.backgroundTexture:SetTexture(nil)
frame.backgroundTexture:SetTexCoord(0, 1, 0, 1)
frame.backgroundTexture:SetVertexColor(1, 1, 1, 1)
if self._background == nil then
frame.backgroundTexture:Hide()
elseif type(self._background) == "string" and TSM.UI.TexturePacks.IsValid(self._background) then
-- this is a texture pack
frame.backgroundTexture:Show()
frame.backgroundTexture:ClearAllPoints()
frame.backgroundTexture:SetPoint("CENTER")
TSM.UI.TexturePacks.SetTextureAndSize(frame.backgroundTexture, self._background)
elseif type(self._background) == "string" and strmatch(self._background, "^[ip]:%d+") then
-- this is an itemString
frame.backgroundTexture:Show()
frame.backgroundTexture:ClearAllPoints()
frame.backgroundTexture:SetAllPoints()
frame.backgroundTexture:SetTexture(ItemInfo.GetTexture(self._background))
elseif type(self._background) == "string" then
-- this is a theme color key
frame.backgroundTexture:Show()
frame.backgroundTexture:ClearAllPoints()
frame.backgroundTexture:SetAllPoints()
frame.backgroundTexture:SetColorTexture(Theme.GetColor(self._background):GetFractionalRGBA())
elseif type(self._background) == "number" then
-- this is a wow file id
frame.backgroundTexture:Show()
frame.backgroundTexture:ClearAllPoints()
frame.backgroundTexture:SetAllPoints()
frame.backgroundTexture:SetTexture(self._background)
else
error("Invalid background: "..tostring(self._background))
end
-- set the text color
local textColor = frame:IsEnabled() and self:_GetTextColor() or Theme.GetColor("ACTIVE_BG_ALT")
frame.text:SetTextColor(textColor:GetFractionalRGBA())
-- set the highlight texture
if self._highlightEnabled then
frame.highlight:SetColorTexture(Theme.GetColor(self._background):GetTint("+HOVER"):GetFractionalRGBA())
else
frame.highlight:SetColorTexture(0, 0, 0, 0)
end
if self._iconTexturePack then
TSM.UI.TexturePacks.SetTextureAndSize(frame.icon, self._iconTexturePack)
frame.icon:Show()
frame.icon:ClearAllPoints()
frame.icon:SetVertexColor(textColor:GetFractionalRGBA())
local iconWidth = TSM.UI.TexturePacks.GetWidth(self._iconTexturePack) + ICON_SPACING
if self._iconPosition == "LEFT" then
frame.icon:SetPoint("RIGHT", frame.text, "LEFT", -ICON_SPACING, 0)
frame.text:ClearAllPoints()
if self._justifyH == "CENTER" then
local xOffset = iconWidth / 2
frame.text:SetPoint("TOP", xOffset, -self:_GetPadding("TOP"))
frame.text:SetPoint("BOTTOM", xOffset, self:_GetPadding("BOTTOM"))
frame.text:SetWidth(frame.text:GetStringWidth())
elseif self._justifyH == "LEFT" then
frame.text:SetPoint("TOPLEFT", iconWidth + self:_GetPadding("LEFT"), -self:_GetPadding("TOP"))
frame.text:SetPoint("BOTTOMRIGHT", -self:_GetPadding("RIGHT"), self:_GetPadding("BOTTOM"))
else
error("Unsupported justifyH: "..tostring(self._justifyH))
end
elseif self._iconPosition == "LEFT_NO_TEXT" then
frame.icon:SetPoint("LEFT", self:_GetPadding("LEFT"), 0)
frame.text:ClearAllPoints()
frame.text:Hide()
elseif self._iconPosition == "CENTER" then
frame.icon:SetPoint("CENTER")
frame.text:ClearAllPoints()
frame.text:Hide()
elseif self._iconPosition == "RIGHT" then
frame.icon:SetPoint("RIGHT", -self:_GetPadding("RIGHT"), 0)
local xOffset = iconWidth
frame.text:ClearAllPoints()
-- TODO: support non-left-aligned text
frame.text:SetPoint("TOPLEFT", self:_GetPadding("LEFT"), -self:_GetPadding("TOP"))
frame.text:SetPoint("BOTTOMRIGHT", -xOffset, self:_GetPadding("BOTTOM"))
else
error("Invalid iconPosition: "..tostring(self._iconPosition))
end
else
frame.icon:Hide()
frame.text:ClearAllPoints()
frame.text:SetPoint("TOPLEFT", self:_GetPadding("LEFT"), -self:_GetPadding("TOP"))
frame.text:SetPoint("BOTTOMRIGHT", -self:_GetPadding("RIGHT"), self:_GetPadding("BOTTOM"))
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Button._GetMinimumDimension(self, dimension)
if dimension == "WIDTH" and self._autoWidth then
return self:GetStringWidth() + (self._iconTexturePack and TSM.UI.TexturePacks.GetWidth(self._iconTexturePack) or 0)
else
return self.__super:_GetMinimumDimension(dimension)
end
end

View File

@@ -0,0 +1,245 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Checkbox UI Element Class.
-- This is a simple checkbox element with an attached description text. It is a subclass of the @{Text} class.
-- @classmod Checkbox
local _, TSM = ...
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Theme = TSM.Include("Util.Theme")
local UIElements = TSM.Include("UI.UIElements")
local Checkbox = TSM.Include("LibTSMClass").DefineClass("Checkbox", TSM.UI.Text)
UIElements.Register(Checkbox)
TSM.UI.Checkbox = Checkbox
local private = {}
local THEME_TEXTURES = {
RADIO = {
checked = "iconPack.Misc/Radio/Checked",
unchecked = "iconPack.Misc/Radio/Unchecked",
},
CHECK = {
checked = "iconPack.Misc/Checkbox/Checked",
unchecked = "iconPack.Misc/Checkbox/Unchecked",
},
}
local CHECKBOX_SPACING = 4
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Checkbox.__init(self)
local frame = UIElements.CreateFrame(self, "Button")
self.__super:__init(frame)
ScriptWrapper.Set(frame, "OnClick", private.FrameOnClick, self)
-- create the text and check texture
frame.text = UIElements.CreateFontString(self, frame)
frame.text:SetJustifyV("MIDDLE")
frame.check = frame:CreateTexture()
self._position = "LEFT"
self._theme = "CHECK"
self._font = "BODY_BODY3"
self._disabled = false
self._value = false
self._onValueChangedHandler = nil
self._settingTable = nil
self._settingKey = nil
end
function Checkbox.Release(self)
self._position = "LEFT"
self._theme = "CHECK"
self._disabled = false
self._value = false
self._onValueChangedHandler = nil
self._settingTable = nil
self._settingKey = nil
self.__super:Release()
self._font = "BODY_BODY3"
end
--- Sets the position of the checkbox relative to the text.
-- This method can be used to set the checkbox to be either on the left or right side of the text.
-- @tparam Checkbox self The checkbox object
-- @tparam string position The position of the checkbox relative to the text
-- @treturn Checkbox The checkbox object
function Checkbox.SetCheckboxPosition(self, position)
if position == "LEFT" or position == "RIGHT" then
self._position = position
else
error("Invalid checkbox position: "..tostring(position))
end
return self
end
--- Sets the checkbox theme
-- @tparam Checkbox self The checkbox object
-- @tparam string theme Either "RADIO" or "CHECK"
-- @treturn Checkbox The checkbox object
function Checkbox.SetTheme(self, theme)
assert(THEME_TEXTURES[theme])
self._theme = theme
return self
end
--- Sets whether or not the checkbox is disabled.
-- @tparam Checkbox self The checkbox object
-- @tparam boolean disabled Whether or not the checkbox is disabled
-- @treturn Checkbox The checkbox object
function Checkbox.SetDisabled(self, disabled)
self._disabled = disabled
return self
end
--- Sets the text string.
-- @tparam Checkbox self The checkbox object
-- @tparam string text The text string to be displayed
-- @treturn Checkbox The checkbox object
function Checkbox.SetText(self, text)
self._textStr = text
return self
end
--- Gets the text string.
-- @tparam Checkbox self The checkbox object
-- @treturn string The text string
function Checkbox.GetText(self)
return self._textStr
end
--- Sets a formatted text string.
-- @tparam Checkbox self The checkbox object
-- @tparam vararg ... The format string and arguments
-- @treturn Checkbox The checkbox object
function Checkbox.SetFormattedText(self, ...)
self._textStr = format(...)
return self
end
--- Sets whether or not the checkbox is checked.
-- @tparam Checkbox self The checkbox object
-- @tparam boolean value Whether or not the checkbox is checked
-- @tparam[opt=false] boolean silent If true, will not trigger the `OnValueChanged` script
-- @treturn Checkbox The checkbox object
function Checkbox.SetChecked(self, value, silent)
self._value = value and true or false
if self._onValueChangedHandler and not silent then
self:_onValueChangedHandler(value)
end
return self
end
--- Sets the setting info.
-- This method is used to have the state of the checkbox automatically correspond with the boolean state of a field in
-- a table. This is useful for checkboxes which are tied directly to settings.
-- @tparam Checkbox self The checkbox object
-- @tparam table tbl The table which the field to set belongs to
-- @tparam string key The key into the table to be set based on the checkbox state
-- @treturn Checkbox The checkbox object
function Checkbox.SetSettingInfo(self, tbl, key)
self._settingTable = tbl
self._settingKey = key
self:SetChecked(tbl[key])
return self
end
--- Gets the checked state.
-- @tparam Checkbox self The checkbox object
-- @treturn boolean Whether or not the checkbox is checked
function Checkbox.IsChecked(self)
return self._value
end
--- Registers a script handler.
-- @tparam Checkbox self The checkbox object
-- @tparam string script The script to register for (supported scripts: `OnValueChanged`)
-- @tparam function handler The script handler which will be called with the checkbox object followed by any arguments
-- to the script
-- @treturn Checkbox The checkbox object
function Checkbox.SetScript(self, script, handler)
if script == "OnValueChanged" then
self._onValueChangedHandler = handler
elseif script == "OnEnter" or script == "OnLeave" then
return self.__super:SetScript(script, handler)
else
error("Unknown Checkbox script: "..tostring(script))
end
return self
end
function Checkbox.Draw(self)
self.__super:Draw()
local frame = self:_GetBaseFrame()
if self._disabled then
frame.text:SetTextColor(Theme.GetColor("TEXT_DISABLED"):GetFractionalRGBA())
else
frame.text:SetTextColor(self:_GetTextColor():GetFractionalRGBA())
end
TSM.UI.TexturePacks.SetTextureAndSize(frame.check, THEME_TEXTURES[self._theme][self._value and "checked" or "unchecked"])
frame.text:ClearAllPoints()
frame.check:ClearAllPoints()
if self._position == "LEFT" then
frame.check:SetPoint("LEFT")
frame.text:SetJustifyH("LEFT")
frame.text:SetPoint("LEFT", frame.check, "RIGHT", CHECKBOX_SPACING, 0)
frame.text:SetPoint("TOPRIGHT")
frame.text:SetPoint("BOTTOMRIGHT")
elseif self._position == "RIGHT" then
frame.check:SetPoint("RIGHT")
frame.text:SetJustifyH("RIGHT")
frame.text:SetPoint("BOTTOMLEFT")
frame.text:SetPoint("TOPLEFT")
frame.text:SetPoint("RIGHT", frame.check, "LEFT", -CHECKBOX_SPACING, 0)
else
error("Invalid position: "..tostring(self._position))
end
if self._disabled then
frame.check:SetAlpha(0.3)
self:_GetBaseFrame():Disable()
else
frame.check:SetAlpha(1)
self:_GetBaseFrame():Enable()
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Checkbox._GetMinimumDimension(self, dimension)
if dimension == "WIDTH" and self._autoWidth then
local checkboxWidth = TSM.UI.TexturePacks.GetWidth(THEME_TEXTURES[self._theme].checked)
return self:GetStringWidth() + CHECKBOX_SPACING + checkboxWidth, nil
else
return self.__super:_GetMinimumDimension(dimension)
end
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.FrameOnClick(self)
local value = not self._value
if self._settingTable and self._settingKey then
self._settingTable[self._settingKey] = value
end
self:SetChecked(value)
self:Draw()
end

View File

@@ -0,0 +1,135 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Collapsible Container UI Element Class.
-- An collapsible container is a container which can be collapsed to a single heading line. It is a subclass of the @{Frame} class.
-- @classmod CollapsibleContainer
local _, TSM = ...
local CollapsibleContainer = TSM.Include("LibTSMClass").DefineClass("CollapsibleContainer", TSM.UI.Frame)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(CollapsibleContainer)
TSM.UI.CollapsibleContainer = CollapsibleContainer
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function CollapsibleContainer.__init(self)
self.__super:__init()
self._headingText = ""
self._contextTbl = nil
self._contextKey = nil
end
function CollapsibleContainer.Acquire(self)
self.__super:Acquire()
self:SetBackgroundColor("PRIMARY_BG_ALT", true)
self.__super:SetLayout("VERTICAL")
self.__super:SetPadding(12, 12, 8, 8)
self.__super:AddChild(UIElements.New("Frame", "heading")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Button", "expander")
:SetMargin(0, 4, 0, 0)
:SetScript("OnClick", private.OnExpanderClick)
)
:AddChild(UIElements.New("Text", "text")
:SetFont("BODY_BODY1_BOLD")
)
)
self.__super:AddChild(UIElements.New("Frame", "content"))
end
function CollapsibleContainer.Release(self)
self._headingText = ""
self._contextTbl = nil
self._contextKey = nil
self.__super:Release()
end
--- Sets the context table and key where to store the collapsed state.
-- @tparam CollapsibleContainer self The collapsible container object
-- @tparam table tbl The table
-- @tparam string key The key
-- @treturn CollapsibleContainer The collapsible container object
function CollapsibleContainer.SetContextTable(self, tbl, key)
assert(type(tbl) == "table" and type(key) == "string")
self._contextTbl = tbl
self._contextKey = key
if self._contextTbl[self._contextKey] then
self:GetElement("content"):Hide()
else
self:GetElement("content"):Show()
end
return self
end
--- Set the heading text.
-- @tparam CollapsibleContainer self The collapsible container object
-- @tparam ?string|number headingText The heading text
-- @treturn CollapsibleContainer The collapsible container object
function CollapsibleContainer.SetHeadingText(self, headingText)
assert(type(headingText) == "string" or type(headingText) == "number")
self._headingText = headingText
return self
end
function CollapsibleContainer.SetPadding(self, left, right, top, bottom)
error("CollapsibleContainer doesn't support this method")
end
function CollapsibleContainer.SetLayout(self, layout)
self:GetElement("content"):SetLayout(layout)
return self
end
function CollapsibleContainer.AddChild(self, child)
self:GetElement("content"):AddChild(child)
return self
end
function CollapsibleContainer.AddChildIf(self, condition, child)
self:GetElement("content"):AddChildIf(condition, child)
return self
end
function CollapsibleContainer.AddChildrenWithFunction(self, func, ...)
self:GetElement("content"):AddChildrenWithFunction(func, ...)
return self
end
function CollapsibleContainer.AddChildBeforeById(self, beforeId, child)
self:GetElement("content"):AddChildBeforeById(beforeId, child)
return self
end
function CollapsibleContainer.Draw(self)
self:GetElement("heading.text"):SetText(self._headingText)
self:GetElement("heading.expander"):SetBackgroundAndSize(self._contextTbl[self._contextKey] and "iconPack.18x18/Caret/Right" or "iconPack.18x18/Caret/Down")
self.__super:Draw()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnExpanderClick(button)
local self = button:GetParentElement():GetParentElement()
self._contextTbl[self._contextKey] = not self._contextTbl[self._contextKey]
if self._contextTbl[self._contextKey] then
self:GetElement("content"):Hide()
else
self:GetElement("content"):Show()
end
-- TODO: is there a better way to notify the elements up the stack that our size has changed?
self:GetBaseElement():Draw()
end

View File

@@ -0,0 +1,254 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Commodity List UI Element Class.
-- The element used to show the details of a selected commodity in shopping. It is a subclass of the @{ScrollingTable} class.
-- @classmod CommodityList
local _, TSM = ...
local L = TSM.Include("Locale").GetTable()
local Money = TSM.Include("Util.Money")
local Math = TSM.Include("Util.Math")
local Theme = TSM.Include("Util.Theme")
local UIElements = TSM.Include("UI.UIElements")
local CommodityList = TSM.Include("LibTSMClass").DefineClass("CommodityList", TSM.UI.ScrollingTable)
UIElements.Register(CommodityList)
TSM.UI.CommodityList = CommodityList
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function CommodityList.__init(self)
self.__super:__init()
self._row = nil
self._marketValueFunc = nil
self._alertThreshold = math.huge
end
function CommodityList.Acquire(self)
self._headerHidden = true
self.__super:Acquire()
self:GetScrollingTableInfo()
:NewColumn("warning")
:SetWidth(12)
:SetIconSize(12)
:SetIconHoverEnabled(true)
:SetIconFunction(private.GetWarningIcon)
:SetJustifyH("CENTER")
:SetFont("BODY_BODY3")
:Commit()
:NewColumn("itemBuyout")
:SetFont("TABLE_TABLE1")
:SetJustifyH("LEFT")
:SetTextFunction(private.GetItemBuyoutText)
:DisableHiding()
:Commit()
:NewColumn("quantity")
:SetWidth(60)
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextFunction(private.GetQuantityText)
:DisableHiding()
:Commit()
:NewColumn("pct")
:SetWidth(50)
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextFunction(private.GetPercentText)
:DisableHiding()
:Commit()
:Commit()
end
function CommodityList.Release(self)
self._row = nil
self._marketValueFunc = nil
self._alertThreshold = math.huge
self.__super:Release()
end
function CommodityList.GetTotalQuantity(self, maxIndex)
local totalQuantity = 0
for _, index in ipairs(self._data) do
if index > maxIndex then
break
end
local subRow = self:_GetSubRow(index)
local _, numOwnerItems = subRow:GetOwnerInfo()
local quantityAvailable = subRow:GetQuantities() - numOwnerItems
totalQuantity = totalQuantity + quantityAvailable
end
return totalQuantity
end
--- Sets the result row.
-- @tparam CommodityList self The commodity list object
-- @tparam table row The row to set
-- @treturn CommodityList The commodity list object
function CommodityList.SetData(self, row)
self._row = row
self:UpdateData()
return self
end
--- Sets the selected quantity.
-- @tparam CommodityList self The commodity list object
-- @tparam number quantity The selected quantity
-- @treturn CommodityList The commodity list object
function CommodityList.SelectQuantity(self, quantity)
local maxIndex = nil
for _, index in ipairs(self._data) do
local subRow = self:_GetSubRow(index)
local _, numOwnerItems = subRow:GetOwnerInfo()
local quantityAvailable = subRow:GetQuantities() - numOwnerItems
maxIndex = index
quantity = quantity - quantityAvailable
if quantity <= 0 then
break
end
end
self:SetSelection(maxIndex)
return self
end
--- Sets the market value function.
-- @tparam CommodityList self The commodity list object
-- @tparam function func The function to call with the ResultSubRow to get the market value
-- @treturn CommodityList The commodity list object
function CommodityList.SetMarketValueFunction(self, func)
self._marketValueFunc = func
return self
end
--- Sets the alert threshold.
-- @tparam CommodityList self The commodity list object
-- @tparam number threshold The item buyout above which the alert icon should be shown
-- @treturn CommodityList The commodity list object
function CommodityList.SetAlertThreshold(self, threshold)
self._alertThreshold = threshold or math.huge
return self
end
function CommodityList.SetSelection(self, selection)
self.__super:SetSelection(selection and self:_SanitizeSelectionIndex(selection) or nil)
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function CommodityList._HandleRowClick(self, data, mouseButton)
local index = self:_SanitizeSelectionIndex(data)
if not index then
return
end
self.__super:_HandleRowClick(index, mouseButton)
end
function CommodityList._SanitizeSelectionIndex(self, selectedIndex)
-- select the highest subrow which isn't the player's auction and isn't above the selection
local highestIndex = nil
for index, subRow in self._row:SubRowIterator() do
if subRow:GetQuantities() - select(2, subRow:GetOwnerInfo()) ~= 0 then
highestIndex = index
end
if index == selectedIndex then
break
end
end
return highestIndex
end
function CommodityList._GetSubRow(self, index)
return self._row._subRows[index]
end
function CommodityList._UpdateData(self)
wipe(self._data)
if not self._row then
return
end
for index in self._row:SubRowIterator() do
tinsert(self._data, index)
end
end
function CommodityList._IsSelected(self, data)
if data > (self._selection or 0) then
return false
end
local subRow = self:_GetSubRow(data)
local _, numOwnerItems = subRow:GetOwnerInfo()
local quantityAvailable = subRow:GetQuantities() - numOwnerItems
return quantityAvailable > 0
end
function CommodityList._GetMarketValuePct(self, row)
assert(row:IsSubRow())
if not self._marketValueFunc then
-- no market value function was set
return nil, nil
end
local marketValue = self._marketValueFunc(row) or 0
if marketValue == 0 then
-- this item doesn't have a market value
return nil, nil
end
local _, itemBuyout = row:GetBuyouts()
return itemBuyout > 0 and Math.Round(100 * itemBuyout / marketValue) or nil
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetWarningIcon(self, index)
local subRow = self:_GetSubRow(index)
assert(subRow)
local _, itemBuyout = subRow:GetBuyouts()
if itemBuyout < self._alertThreshold then
return
end
return "iconPack.12x12/Attention", L["This price is above your confirmation alert threshold."]
end
function private.GetItemBuyoutText(self, index)
local _, itemBuyout = self:_GetSubRow(index):GetBuyouts()
return Money.ToString(itemBuyout, nil, "OPT_83_NO_COPPER")
end
function private.GetPercentText(self, index)
local pct = self:_GetMarketValuePct(self:_GetSubRow(index))
if not pct then
return "---"
end
local pctColor = Theme.GetAuctionPercentColor(pct)
if pct > 999 then
pct = ">999"
end
return pctColor:ColorText(pct.."%")
end
function private.GetQuantityText(self, index)
local subRow = self:_GetSubRow(index)
local _, numOwnerItems = subRow:GetOwnerInfo()
local totalQuantity = subRow:GetQuantities()
local quantityAvailable = totalQuantity - numOwnerItems
if quantityAvailable == 0 then
return Theme.GetColor("INDICATOR_ALT"):ColorText(totalQuantity)
else
return quantityAvailable
end
end

View File

@@ -0,0 +1,207 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Container UI Element Class.
-- A container is an abstract element class which simply contains other elements. It is a subclass of the @{Element} class.
-- @classmod Container
local _, TSM = ...
local TempTable = TSM.Include("Util.TempTable")
local Table = TSM.Include("Util.Table")
local Container = TSM.Include("LibTSMClass").DefineClass("Container", TSM.UI.Element, "ABSTRACT")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Container)
TSM.UI.Container = Container
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Container.__init(self, frame)
self.__super:__init(frame)
self._children = {}
self._layoutChildren = {}
self._noLayoutChildren = {}
end
function Container.Release(self)
self:ReleaseAllChildren()
self.__super:Release()
end
--- Release all child elements.
-- @tparam Container self The container object
function Container.ReleaseAllChildren(self)
for _, child in ipairs(self._children) do
child:Release()
end
wipe(self._children)
wipe(self._layoutChildren)
wipe(self._noLayoutChildren)
end
--- Add a child element.
-- @tparam Container self The container object
-- @tparam Element child The child element
-- @treturn Container The container object
function Container.AddChild(self, child)
self:_AddChildHelper(child, true)
return self
end
--- Add a child element when the required condition is true.
-- @tparam Container self The container object
-- @tparam boolean condition The required condition
-- @tparam Element child The child element
-- @treturn Container The container object
function Container.AddChildIf(self, condition, child)
if not condition then
child:Release()
return self
end
self:_AddChildHelper(child, true)
return self
end
--- Add a child element before another one.
-- @tparam Container self The container object
-- @tparam string beforeId The id of the child element to add this one before
-- @tparam Element child The child element
-- @treturn Container The container object
function Container.AddChildBeforeById(self, beforeId, child)
self:_AddChildHelper(child, true, beforeId)
return self
end
--- Add child elements using a function.
-- @tparam Container self The container object
-- @tparam function func The function to call and pass this container object
-- @tparam vararg ... Additional arguments to pass to the function
-- @treturn Container The container object
function Container.AddChildrenWithFunction(self, func, ...)
func(self, ...)
return self
end
--- Add a child element which is not involved in layout.
-- The layout of this child must be explicitly done by the application code.
-- @tparam Container self The container object
-- @tparam Element child The child element
-- @treturn Container The container object
function Container.AddChildNoLayout(self, child)
self:_AddChildHelper(child, false)
return self
end
--- Remove a child element.
-- @tparam Container self The container object
-- @tparam Element child The child element to remove
function Container.RemoveChild(self, child)
assert(child:__isa(TSM.UI.Element) and child:_GetBaseFrame():GetParent())
child:_GetBaseFrame():SetParent(nil)
Table.RemoveByValue(self._children, child)
Table.RemoveByValue(self._layoutChildren, child)
Table.RemoveByValue(self._noLayoutChildren, child)
child:_SetParentElement(nil)
end
function Container.HasChildById(self, childId)
for _, child in ipairs(self._children) do
if child._id == childId then
return true
end
end
return false
end
--- Gets the number of child elements involved in layout.
-- @tparam Container self The container object
-- @treturn number The number of elements
function Container.GetNumLayoutChildren(self)
local count = 0
for _ in self:LayoutChildrenIterator() do
count = count + 1
end
return count
end
--- Iterates through the child elements involved in layout.
-- @tparam Container self The container object
-- @return An iterator with the following fields: `index, child`
function Container.LayoutChildrenIterator(self)
local children = TempTable.Acquire()
for _, child in ipairs(self._layoutChildren) do
if child:IsVisible() then
tinsert(children, child)
end
end
return TempTable.Iterator(children)
end
--- Shows all child elements.
-- @tparam Container self The container object
function Container.ShowAllChildren(self)
for _, child in ipairs(self._layoutChildren) do
if not child:IsVisible() then
child:Show()
end
end
end
function Container.Draw(self)
self.__super:Draw()
for _, child in ipairs(self._children) do
child:Draw()
end
end
-- ============================================================================
-- Container - Private Class Methods
-- ============================================================================
function Container._AddChildHelper(self, child, layout, beforeId)
assert(child:__isa(TSM.UI.Element) and not child:_GetBaseFrame():GetParent())
child:_GetBaseFrame():SetParent(self:_GetBaseFrame())
tinsert(self._children, private.GetElementInsertIndex(self._children, beforeId), child)
if layout then
tinsert(self._layoutChildren, private.GetElementInsertIndex(self._layoutChildren, beforeId), child)
else
tinsert(self._noLayoutChildren, private.GetElementInsertIndex(self._noLayoutChildren, beforeId), child)
end
child:_SetParentElement(self)
child:Show()
end
function Container._ClearBaseElementCache(self)
self.__super:_ClearBaseElementCache()
for _, child in ipairs(self._children) do
child:_ClearBaseElementCache()
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetElementInsertIndex(tbl, beforeId)
if not beforeId then
return #tbl + 1
end
for i, element in ipairs(tbl) do
if element._id == beforeId then
return i
end
end
error("Invalid beforeId: "..tostring(beforeId))
end

View File

@@ -0,0 +1,141 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Crafting Mat List UI Element Class.
-- The element used to show the mats for a specific craft in the Crafting UI. It is a subclass of the @{ScrollingTable} class.
-- @classmod CraftingMatList
local _, TSM = ...
local CraftingMatList = TSM.Include("LibTSMClass").DefineClass("CraftingMatList", TSM.UI.ScrollingTable)
local ItemString = TSM.Include("Util.ItemString")
local Theme = TSM.Include("Util.Theme")
local Inventory = TSM.Include("Service.Inventory")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(CraftingMatList)
TSM.UI.CraftingMatList = CraftingMatList
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function CraftingMatList.__init(self)
self.__super:__init()
self._spellId = nil
self._rowHoverEnabled = false
end
function CraftingMatList.Acquire(self)
self._headerHidden = true
self.__super:Acquire()
self:SetSelectionDisabled(true)
self:GetScrollingTableInfo()
:NewColumn("check")
:SetWidth(14)
:SetIconSize(14)
:SetIconFunction(private.GetCheck)
:Commit()
:NewColumn("item")
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetIconSize(12)
:SetIconFunction(private.GetItemIcon)
:SetTextFunction(private.GetItemText)
:SetTooltipFunction(private.GetItemTooltip)
:Commit()
:NewColumn("qty")
:SetAutoWidth()
:SetFont("TABLE_TABLE1")
:SetJustifyH("CENTER")
:SetTextFunction(private.GetQty)
:Commit()
:Commit()
end
function CraftingMatList.Release(self)
self._spellId = nil
self.__super:Release()
end
function CraftingMatList.SetScript(self, script, handler)
error("Unknown CraftingMatList script: "..tostring(script))
return self
end
--- Sets the crafting recipe to display materials for.
-- @tparam CraftingMatList self The crafting mat list object
-- @tparam number spellId The spellId for the recipe
-- @treturn CraftingMatList The crafting mat list object
function CraftingMatList.SetRecipe(self, spellId)
self._spellId = spellId
self:_UpdateData()
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function CraftingMatList._UpdateData(self)
wipe(self._data)
if not self._spellId then
return
end
for i = 1, TSM.Crafting.ProfessionUtil.GetNumMats(self._spellId) do
tinsert(self._data, i)
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetCheck(self, index)
local itemLink, _, _, quantity = TSM.Crafting.ProfessionUtil.GetMatInfo(self._spellId, index)
local itemString = ItemString.Get(itemLink)
local bagQuantity = Inventory.GetBagQuantity(itemString)
if not TSM.IsWowClassic() then
bagQuantity = bagQuantity + Inventory.GetReagentBankQuantity(itemString) + Inventory.GetBankQuantity(itemString)
end
if bagQuantity >= quantity then
return TSM.UI.TexturePacks.GetColoredKey("iconPack.14x14/Checkmark/Default", Theme.GetFeedbackColor("GREEN"))
else
return TSM.UI.TexturePacks.GetColoredKey("iconPack.14x14/Close/Default", Theme.GetFeedbackColor("RED"))
end
end
function private.GetItemIcon(self, index)
local _, _, texture = TSM.Crafting.ProfessionUtil.GetMatInfo(self._spellId, index)
return texture
end
function private.GetItemText(self, index)
local itemLink = TSM.Crafting.ProfessionUtil.GetMatInfo(self._spellId, index)
local itemString = ItemString.Get(itemLink)
return TSM.UI.GetColoredItemName(itemString) or Theme.GetFeedbackColor("RED"):ColorText("?")
end
function private.GetItemTooltip(self, index)
local itemLink = TSM.Crafting.ProfessionUtil.GetMatInfo(self._spellId, index)
return ItemString.Get(itemLink)
end
function private.GetQty(self, index)
local itemLink, _, _, quantity = TSM.Crafting.ProfessionUtil.GetMatInfo(self._spellId, index)
local itemString = ItemString.Get(itemLink)
local bagQuantity = Inventory.GetBagQuantity(itemString)
if not TSM.IsWowClassic() then
bagQuantity = bagQuantity + Inventory.GetReagentBankQuantity(itemString) + Inventory.GetBankQuantity(itemString)
end
local color = bagQuantity >= quantity and Theme.GetFeedbackColor("GREEN") or Theme.GetFeedbackColor("RED")
return color:ColorText(format("%d / %d", bagQuantity, quantity))
end

View File

@@ -0,0 +1,498 @@
-- ------------------------------------------------------------------------------ --
-- 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

View File

@@ -0,0 +1,281 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Divided Container UI Element Class.
-- A divided container is a container with two children with a divider between them. It is a subclass of the @{Frame} class.
-- @classmod DividedContainer
local _, TSM = ...
local UIElements = TSM.Include("UI.UIElements")
local DividedContainer = TSM.Include("LibTSMClass").DefineClass("DividedContainer", TSM.UI.Frame)
UIElements.Register(DividedContainer)
TSM.UI.DividedContainer = DividedContainer
local private = {}
local DIVIDER_SIZE = 2
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function DividedContainer.__init(self)
self.__super:__init()
self._leftChild = nil
self._rightChild = nil
self._resizeStartX = nil
self._resizeOffset = 0
self._contextTable = nil
self._defaultContextTable = nil
self._minLeftWidth = nil
self._minRightWidth = nil
end
function DividedContainer.Acquire(self)
self.__super:AddChildNoLayout(UIElements.New("Frame", "leftEmpty")
:AddAnchor("TOPLEFT")
:AddAnchor("BOTTOMRIGHT", "divider", "BOTTOMLEFT")
)
self.__super:AddChild(UIElements.New("Button", "divider")
:SetSize(DIVIDER_SIZE, nil)
:SetHitRectInsets(-2, -2, 0, 0)
:SetRelativeLevel(2)
:EnableRightClick()
:SetScript("OnMouseDown", private.HandleOnMouseDown)
:SetScript("OnMouseUp", private.HandleOnMouseUp)
:SetScript("OnClick", private.HandleOnClick)
:SetScript("OnUpdate", private.HandleOnUpdate)
)
self.__super:AddChildNoLayout(UIElements.New("Frame", "rightEmpty")
:AddAnchor("TOPLEFT", "divider", "TOPRIGHT")
:AddAnchor("BOTTOMRIGHT")
)
self.__super:Acquire()
self.__super:SetLayout("HORIZONTAL")
end
function DividedContainer.Release(self)
self._isVertical = false
self._leftChild = nil
self._rightChild = nil
self._resizeStartX = nil
self._resizeOffset = 0
self._contextTable = nil
self._defaultContextTable = nil
self._minLeftWidth = nil
self._minRightWidth = nil
self.__super:Release()
end
function DividedContainer.SetVertical(self)
assert(not self._leftChild and not self._rightChild and not self._isVertical)
self._isVertical = true
self:GetElement("leftEmpty")
:WipeAnchors()
:AddAnchor("TOPLEFT")
:AddAnchor("BOTTOMRIGHT", "divider", "TOPRIGHT")
self:GetElement("divider")
:SetSize(nil, DIVIDER_SIZE)
:SetHitRectInsets(0, 0, -2, -2)
self:GetElement("rightEmpty")
:WipeAnchors()
:AddAnchor("TOPLEFT", "divider", "BOTTOMLEFT")
:AddAnchor("BOTTOMRIGHT")
self.__super:SetLayout("VERTICAL")
return self
end
function DividedContainer.SetLayout(self, layout)
error("DividedContainer doesn't support this method")
end
function DividedContainer.AddChild(self, child)
error("DividedContainer doesn't support this method")
end
function DividedContainer.AddChildBeforeById(self, beforeId, child)
error("DividedContainer doesn't support this method")
end
--- Sets the context table.
-- This table can be used to preserve the divider position across lifecycles of the divided container and even WoW
-- sessions if it's within the settings DB. The position is stored as the width of the left child element.
-- @tparam DividedContainer self The divided container object
-- @tparam table tbl The context table
-- @tparam table defaultTbl The default table (required fields: `leftWidth`)
-- @treturn DividedContainer The divided container object
function DividedContainer.SetContextTable(self, tbl, defaultTbl)
assert(defaultTbl.leftWidth > 0)
tbl.leftWidth = tbl.leftWidth or defaultTbl.leftWidth
self._contextTable = tbl
self._defaultContextTable = defaultTbl
return self
end
--- Sets the context table from a settings object.
-- @tparam DividedContainer self The divided container object
-- @tparam Settings settings The settings object
-- @tparam string key The setting key
-- @treturn DividedContainer The divided container object
function DividedContainer.SetSettingsContext(self, settings, key)
return self:SetContextTable(settings[key], settings:GetDefaultReadOnly(key))
end
--- Sets the minimum width of the child element.
-- @tparam DividedContainer self The divided container object
-- @tparam number minLeftWidth The minimum width of the left child element
-- @tparam number minRightWidth The minimum width of the right child element
-- @treturn DividedContainer The divided container object
function DividedContainer.SetMinWidth(self, minLeftWidth, minRightWidth)
self._minLeftWidth = minLeftWidth
self._minRightWidth = minRightWidth
return self
end
--- Sets the left child element.
-- @tparam DividedContainer self The divided container object
-- @tparam Element child The left child element
-- @treturn DividedContainer The divided container object
function DividedContainer.SetLeftChild(self, child)
assert(not self._isVertical and not self._leftChild and child)
self._leftChild = child
self.__super:AddChildBeforeById("divider", child)
return self
end
--- Sets the right child element.
-- @tparam DividedContainer self The divided container object
-- @tparam Element child The right child element
-- @treturn DividedContainer The divided container object
function DividedContainer.SetRightChild(self, child)
assert(not self._isVertical and not self._rightChild and child)
self._rightChild = child
self.__super:AddChild(child)
return self
end
--- Sets the top child element in vertical mode.
-- @tparam DividedContainer self The divided container object
-- @tparam Element child The top child element
-- @treturn DividedContainer The divided container object
function DividedContainer.SetTopChild(self, child)
assert(self._isVertical and not self._leftChild and child)
self._leftChild = child
self.__super:AddChildBeforeById("divider", child)
return self
end
--- Sets the bottom child element in vertical mode.
-- @tparam DividedContainer self The divided container object
-- @tparam Element child The bottom child element
-- @treturn DividedContainer The divided container object
function DividedContainer.SetBottomChild(self, child)
assert(self._isVertical and not self._rightChild and child)
self._rightChild = child
self.__super:AddChild(child)
return self
end
function DividedContainer.Draw(self)
assert(self._contextTable and self._minLeftWidth and self._minRightWidth)
self.__super.__super.__super:Draw()
self:GetElement("divider")
:SetBackground("ACTIVE_BG")
:SetHighlightEnabled(true)
local width = self:_GetDimension(self._isVertical and "HEIGHT" or "WIDTH") - DIVIDER_SIZE
local leftWidth = self._contextTable.leftWidth + self._resizeOffset
local rightWidth = width - leftWidth
if rightWidth < self._minRightWidth then
leftWidth = width - self._minRightWidth
assert(leftWidth >= self._minLeftWidth)
elseif leftWidth < self._minLeftWidth then
leftWidth = self._minLeftWidth
end
self._contextTable.leftWidth = leftWidth - self._resizeOffset
local leftChild = self._leftChild
local rightChild = self._rightChild
local leftEmpty = self:GetElement("leftEmpty")
local rightEmpty = self:GetElement("rightEmpty")
if self._isVertical then
leftEmpty:SetHeight(leftWidth)
leftChild:SetHeight(leftWidth)
else
leftEmpty:SetWidth(leftWidth)
leftChild:SetWidth(leftWidth)
end
if self._resizeStartX then
leftChild:_GetBaseFrame():SetAlpha(0)
leftChild:_GetBaseFrame():SetFrameStrata("LOW")
rightChild:_GetBaseFrame():SetAlpha(0)
rightChild:_GetBaseFrame():SetFrameStrata("LOW")
leftEmpty:Show()
rightEmpty:Show()
else
leftChild:_GetBaseFrame():SetAlpha(1)
leftChild:_GetBaseFrame():SetFrameStrata(self:_GetBaseFrame():GetFrameStrata())
rightChild:_GetBaseFrame():SetAlpha(1)
rightChild:_GetBaseFrame():SetFrameStrata(self:_GetBaseFrame():GetFrameStrata())
leftEmpty:Hide()
rightEmpty:Hide()
end
self.__super:Draw()
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.HandleOnUpdate(handle)
local self = handle:GetParentElement()
if self._resizeStartX then
if self._isVertical then
local currY = select(2, GetCursorPosition()) / self:_GetBaseFrame():GetEffectiveScale()
self._resizeOffset = self._resizeStartX - currY
else
local currX = GetCursorPosition() / self:_GetBaseFrame():GetEffectiveScale()
self._resizeOffset = currX - self._resizeStartX
end
self:Draw()
end
end
function private.HandleOnMouseDown(handle, mouseButton)
if mouseButton ~= "LeftButton" then
return
end
local self = handle:GetParentElement()
if self._isVertical then
self._resizeStartX = select(2, GetCursorPosition()) / self:_GetBaseFrame():GetEffectiveScale()
else
self._resizeStartX = GetCursorPosition() / self:_GetBaseFrame():GetEffectiveScale()
end
self._resizeOffset = 0
end
function private.HandleOnMouseUp(handle, mouseButton)
if mouseButton ~= "LeftButton" then
return
end
local self = handle:GetParentElement()
self._contextTable.leftWidth = max(self._contextTable.leftWidth + self._resizeOffset, self._minLeftWidth)
self._resizeOffset = 0
self._resizeStartX = nil
self:Draw()
end
function private.HandleOnClick(handle, mouseButton)
if mouseButton ~= "RightButton" then
return
end
local self = handle:GetParentElement()
self._contextTable.leftWidth = self._defaultContextTable.leftWidth
self:Draw()
end

View File

@@ -0,0 +1,195 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Color = TSM.Include("Util.Color")
local Theme = TSM.Include("Util.Theme")
local UIElements = TSM.Include("UI.UIElements")
local DropdownList = TSM.Include("LibTSMClass").DefineClass("DropdownList", TSM.UI.ScrollingTable)
UIElements.Register(DropdownList)
TSM.UI.DropdownList = DropdownList
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function DropdownList.__init(self)
self.__super:__init()
self._selectedItems = {}
self._multiselect = false
self._onSelectionChangedHandler = nil
end
function DropdownList.Acquire(self)
self._backgroundColor = "ACTIVE_BG"
self._headerHidden = true
self.__super:Acquire()
self:SetSelectionDisabled(true)
self:GetScrollingTableInfo()
:NewColumn("text")
:SetFont("BODY_BODY3")
:SetJustifyH("LEFT")
:SetTextFunction(private.GetText)
:SetIconSize(12)
:SetIconFunction(private.GetIcon)
:DisableHiding()
:Commit()
:Commit()
end
function DropdownList.Release(self)
wipe(self._selectedItems)
self._multiselect = false
self._onSelectionChangedHandler = nil
self.__super:Release()
end
function DropdownList.SetMultiselect(self, multiselect)
self._multiselect = multiselect
return self
end
function DropdownList.SetItems(self, items, selection, redraw)
wipe(self._data)
for _, item in ipairs(items) do
tinsert(self._data, item)
end
self:_SetSelectionHelper(selection)
if redraw then
self:Draw()
end
return self
end
function DropdownList.ItemIterator(self)
return private.ItemIterator, self, 0
end
function DropdownList.SetSelection(self, selection)
self:_SetSelectionHelper(selection)
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler(self._multiselect and self._selectedItems or selection)
end
return self
end
function DropdownList.GetSelection(self)
if self._multiselect then
return self._selectedItems
else
local selectedItem = next(self._selectedItems)
return selectedItem
end
end
function DropdownList.SelectAll(self)
assert(self._multiselect)
for _, data in ipairs(self._data) do
self._selectedItems[data] = true
end
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler(self._selectedItems)
end
self:Draw()
end
function DropdownList.DeselectAll(self)
assert(self._multiselect)
wipe(self._selectedItems)
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler(self._selectedItems)
end
self:Draw()
end
function DropdownList.SetScript(self, script, handler)
if script == "OnSelectionChanged" then
self._onSelectionChangedHandler = handler
else
error("Invalid DropdownList script: "..tostring(script))
end
return self
end
function DropdownList.Draw(self)
self.__super:Draw()
local textColor = nil
local color = Theme.GetColor(self._backgroundColor)
-- the text color should have maximum contrast with the background color, so set it to white/black based on the background color
if color:IsLight() then
-- the background is light, so set the text to black
textColor = Color.GetFullBlack()
else
-- the background is dark, so set the text to white
textColor = Color.GetFullWhite()
end
for _, row in ipairs(self._rows) do
row:SetTextColor(textColor)
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function DropdownList._SetSelectionHelper(self, selection)
wipe(self._selectedItems)
if selection then
if self._multiselect then
assert(type(selection) == "table")
for item, selected in pairs(selection) do
self._selectedItems[item] = selected
end
else
assert(type(selection) == "string" or type(selection) == "number")
self._selectedItems[selection] = true
end
end
end
function DropdownList._HandleRowClick(self, data)
if self._multiselect then
self._selectedItems[data] = not self._selectedItems[data] or nil
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler(self._selectedItems)
end
self:Draw()
else
self:SetSelection(data)
end
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.GetText(self, data)
return data
end
function private.GetIcon(self, data)
return self._multiselect and self._selectedItems[data] and "iconPack.12x12/Checkmark/Default" or ""
end
function private.ItemIterator(self, index)
index = index + 1
local item = self._data[index]
if not item then
return
end
return index, item, self._selectedItems[item]
end

View File

@@ -0,0 +1,179 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- EditableText UI Element Class.
-- A text element which has an editing state. It is a subclass of the @{Text} class.
-- @classmod EditableText
local _, TSM = ...
local Theme = TSM.Include("Util.Theme")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local ItemLinked = TSM.Include("Service.ItemLinked")
local UIElements = TSM.Include("UI.UIElements")
local EditableText = TSM.Include("LibTSMClass").DefineClass("EditableText", TSM.UI.Text)
UIElements.Register(EditableText)
TSM.UI.EditableText = EditableText
local private = {}
local STRING_RIGHT_PADDING = 16
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function EditableText.__init(self)
local frame = UIElements.CreateFrame(self, "EditBox")
self.__super:__init(frame)
frame:SetShadowColor(0, 0, 0, 0)
frame:SetAutoFocus(false)
ScriptWrapper.Set(frame, "OnEscapePressed", private.OnEscapePressed, self)
ScriptWrapper.Set(frame, "OnEnterPressed", private.OnEnterPressed, self)
ScriptWrapper.Set(frame, "OnEditFocusLost", private.OnEditFocusLost, self)
frame.text = UIElements.CreateFontString(self, frame)
frame.text:SetAllPoints()
local function ItemLinkedCallback(name, link)
if self._allowItemInsert == nil or not self:IsVisible() or not self._editing then
return
end
if self._allowItemInsert == true then
frame:Insert(link)
else
frame:Insert(name)
end
return true
end
ItemLinked.RegisterCallback(ItemLinkedCallback, -1)
self._editing = false
self._allowItemInsert = nil
self._onValueChangedHandler = nil
self._onEditingChangedHandler = nil
end
function EditableText.Release(self)
self:_GetBaseFrame():ClearFocus()
self:_GetBaseFrame():Disable()
self._editing = false
self._allowItemInsert = nil
self._onValueChangedHandler = nil
self._onEditingChangedHandler = nil
self.__super:Release()
end
--- Registers a script handler.
-- @tparam EditableText self The editable text object
-- @tparam string script The script to register for (supported scripts: `OnValueChanged`, `OnEditingChanged`)
-- @tparam function handler The script handler which will be called with the editable text object followed by any
-- arguments to the script
-- @treturn EditableText The editable text object
function EditableText.SetScript(self, script, handler)
if script == "OnValueChanged" then
self._onValueChangedHandler = handler
elseif script == "OnEditingChanged" then
self._onEditingChangedHandler = handler
elseif script == "OnEnter" or script == "OnLeave" or script == "OnMouseDown" then
self.__super:SetScript(script, handler)
else
error("Unknown EditableText script: "..tostring(script))
end
return self
end
--- Sets whether or not the text is currently being edited.
-- @tparam EditableText self The editable text object
-- @tparam boolean editing The editing state to set
-- @treturn EditableText The editable text object
function EditableText.SetEditing(self, editing)
self._editing = editing
if self._onEditingChangedHandler then
self:_onEditingChangedHandler(editing)
end
if self._autoWidth then
self:GetParentElement():Draw()
else
self:Draw()
end
return self
end
--- Allows inserting an item into the editable text by linking it while the editable text has focus.
-- @tparam EditableText self The editable text object
-- @tparam[opt=false] boolean insertLink Insert the link instead of the item name
-- @treturn EditableText The editable text object
function EditableText.AllowItemInsert(self, insertLink)
assert(insertLink == true or insertLink == false or insertLink == nil)
self._allowItemInsert = insertLink or false
return self
end
function EditableText.Draw(self)
self.__super:Draw()
local frame = self:_GetBaseFrame()
-- set the editbox font
frame:SetFont(Theme.GetFont(self._font):GetWowFont())
-- set the justification
frame:SetJustifyH(self._justifyH)
frame:SetJustifyV(self._justifyV)
-- set the text color
frame:SetTextColor(self:_GetTextColor():GetFractionalRGBA())
if self._editing then
frame:Enable()
frame:SetText(self._textStr)
frame:SetFocus()
frame:HighlightText(0, -1)
frame.text:Hide()
else
frame:SetText("")
frame:ClearFocus()
frame:HighlightText(0, 0)
frame:Disable()
frame.text:Show()
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function EditableText._GetPreferredDimension(self, dimension)
if dimension == "WIDTH" and self._autoWidth and not self._editing then
return self:GetStringWidth() + STRING_RIGHT_PADDING
else
return self.__super.__super:_GetPreferredDimension(dimension)
end
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnEscapePressed(self)
self:SetEditing(false)
end
function private.OnEnterPressed(self)
local newText = self:_GetBaseFrame():GetText()
self:SetEditing(false)
self:_onValueChangedHandler(newText)
end
function private.OnEditFocusLost(self)
self:SetEditing(false)
end

View File

@@ -0,0 +1,575 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Base UI Element Class.
-- This the base class for all other UI element classes.
-- @classmod Element
local _, TSM = ...
local Element = TSM.Include("LibTSMClass").DefineClass("Element", nil, "ABSTRACT")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Analytics = TSM.Include("Util.Analytics")
local Tooltip = TSM.Include("UI.Tooltip")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Element)
TSM.UI.Element = Element
local private = {}
local ANCHOR_REL_PARENT = newproxy()
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Element.__init(self, frame)
self._tags = {}
self._frame = frame
self._scripts = {}
self._baseElementCache = nil
self._parent = nil
self._context = nil
self._acquired = nil
self._tooltip = nil
self._width = nil
self._height = nil
self._margin = { left = 0, right = 0, top = 0, bottom = 0 }
self._padding = { left = 0, right = 0, top = 0, bottom = 0 }
self._relativeLevel = nil
self._anchors = {}
end
function Element.__tostring(self)
local parentId = self._parent and self._parent._id
return self.__class.__name..":"..(parentId and (parentId..".") or "")..(self._id or "?")
end
function Element.SetId(self, id)
-- should only be called by core UI code before acquiring the element
assert(not self._acquired)
self._id = id or tostring(self)
end
function Element.SetTags(self, ...)
-- should only be called by core UI code before acquiring the element
assert(not self._acquired)
assert(#self._tags == 0)
for i = 1, select("#", ...) do
local tag = select(i, ...)
tinsert(self._tags, tag)
end
end
function Element.Acquire(self)
assert(not self._acquired)
self._acquired = true
self:Show()
end
function Element.Release(self)
assert(self._acquired)
local frame = self:_GetBaseFrame()
-- clear the OnLeave script before hiding the frame (otherwise it'll get called)
if self._scripts.OnLeave then
frame:SetScript("OnLeave", nil)
self._scripts.OnLeave = nil
end
if self._tooltip and Tooltip.IsVisible(frame) then
-- hide the tooltip
Tooltip.Hide()
end
self:Hide()
frame:ClearAllPoints()
frame:SetParent(nil)
frame:SetScale(1)
-- clear scripts
for script in pairs(self._scripts) do
frame:SetScript(script, nil)
end
wipe(self._tags)
wipe(self._scripts)
self._baseElementCache = nil
self._parent = nil
self._context = nil
self._acquired = nil
self._tooltip = nil
self._width = nil
self._height = nil
self._margin.left = 0
self._margin.right = 0
self._margin.top = 0
self._margin.bottom = 0
self._padding.left = 0
self._padding.right = 0
self._padding.top = 0
self._padding.bottom = 0
self._relativeLevel = nil
wipe(self._anchors)
UIElements.Recycle(self)
end
--- Shows the element.
-- @tparam Element self The element object
function Element.Show(self)
self:_GetBaseFrame():Show()
return self
end
--- Hides the element.
-- @tparam Element self The element object
function Element.Hide(self)
self:_GetBaseFrame():Hide()
return self
end
--- Returns whether or not the element is visible.
-- @tparam Element self The element object
-- @treturn boolean Whether or not the element is currently visible
function Element.IsVisible(self)
return self:_GetBaseFrame():IsVisible()
end
--- Sets the width of the element.
-- @tparam Element self The element object
-- @tparam ?number width The width of the element, or nil to have an undefined width
-- @treturn Element The element object
function Element.SetWidth(self, width)
assert(width == nil or type(width) == "number")
self._width = width
return self
end
--- Sets the height of the element.
-- @tparam Element self The element object
-- @tparam ?number height The height of the element, or nil to have an undefined height
-- @treturn Element The element object
function Element.SetHeight(self, height)
assert(height == nil or type(height) == "number")
self._height = height
return self
end
--- Sets the width and height of the element.
-- @tparam Element self The element object
-- @tparam ?number width The width of the element, or nil to have an undefined width
-- @tparam ?number height The height of the element, or nil to have an undefined height
-- @treturn Element The element object
function Element.SetSize(self, width, height)
self:SetWidth(width)
self:SetHeight(height)
return self
end
--- Sets the padding of the element.
-- @tparam Element self The element object
-- @tparam number left The left padding value if all arguments are passed or the value of all sides if a single argument is passed
-- @tparam[opt] number right The right padding value if all arguments are passed
-- @tparam[opt] number top The top padding value if all arguments are passed
-- @tparam[opt] number bottom The bottom padding value if all arguments are passed
-- @treturn Element The element object
function Element.SetPadding(self, left, right, top, bottom)
if not right and not top and not bottom then
right = left
top = left
bottom = left
end
assert(type(left) == "number" and type(right) == "number" and type(top) == "number" and type(bottom) == "number")
self._padding.left = left
self._padding.right = right
self._padding.top = top
self._padding.bottom = bottom
return self
end
--- Sets the margin of the element.
-- @tparam Element self The element object
-- @tparam number left The left margin value if all arguments are passed or the value of all sides if a single argument is passed
-- @tparam[opt] number right The right margin value if all arguments are passed
-- @tparam[opt] number top The top margin value if all arguments are passed
-- @tparam[opt] number bottom The bottom margin value if all arguments are passed
-- @treturn Element The element object
function Element.SetMargin(self, left, right, top, bottom)
if not right and not top and not bottom then
right = left
top = left
bottom = left
end
assert(type(left) == "number" and type(right) == "number" and type(top) == "number" and type(bottom) == "number")
self._margin.left = left
self._margin.right = right
self._margin.top = top
self._margin.bottom = bottom
return self
end
--- Sets the relative level of this element with regards to its parent.
-- @tparam Element self The element object
-- @tparam number level The relative level of this element
-- @treturn Element The element object
function Element.SetRelativeLevel(self, level)
self._relativeLevel = level
return self
end
--- Wipes the element's anchors.
-- @treturn Element The element object
function Element.WipeAnchors(self)
wipe(self._anchors)
return self
end
--- Adds an anchor to the element.
-- @tparam Element self The element object
-- @param ... The anchor arguments (following WoW's SetPoint() arguments)
-- @treturn Element The element object
function Element.AddAnchor(self, ...)
local numArgs = select("#", ...)
local point, relFrame, relPoint, x, y = nil, nil, nil, nil, nil
if numArgs == 1 then
point = ...
elseif numArgs == 2 then
point, relFrame = ...
elseif numArgs == 3 then
local arg2 = select(2, ...)
if type(arg2) == "number" then
point, x, y = ...
else
point, relFrame, relPoint = ...
end
elseif numArgs == 4 then
point, relFrame, x, y = ...
elseif numArgs == 5 then
point, relFrame, relPoint, x, y = ...
else
error("Invalid anchor")
end
tinsert(self._anchors, point)
tinsert(self._anchors, relFrame or ANCHOR_REL_PARENT)
tinsert(self._anchors, relPoint or point)
tinsert(self._anchors, x or 0)
tinsert(self._anchors, y or 0)
return self
end
--- Gets the top-most element in the tree.
-- @tparam Element self The element object
-- @treturn Element The top-most element object
function Element.GetBaseElement(self)
if not self._baseElementCache then
local element = self
local parent = element:GetParentElement()
while parent do
local temp = element
element = parent
parent = temp:GetParentElement()
end
self._baseElementCache = element
end
return self._baseElementCache
end
--- Gets the parent element's base frame.
-- @tparam Element self The element object
-- @treturn Element The parent element's base frame
function Element.GetParent(self)
return self:GetParentElement():_GetBaseFrame()
end
--- Gets the parent element.
-- @tparam Element self The element object
-- @treturn Element The parent element object
function Element.GetParentElement(self)
return self._parent
end
--- Gets another element in the tree by relative path.
-- The path consists of element ids separated by `.`. `__parent` may also be used to indicate the parent element.
-- @tparam Element self The element object
-- @tparam string path The relative path to the element
-- @treturn Element The desired element
function Element.GetElement(self, path)
-- First try to find the element as a child of self
local result = private.GetElementHelper(self, path)
if not result then
Analytics.Action("GET_ELEMENT_FAIL", tostring(self), path)
end
-- TODO: is this needed?
result = result or private.GetElementHelper(self:GetBaseElement(), path)
return result
end
--- Sets the tooltip of the element.
-- @tparam Element self The element object
-- @param tooltip The value passed to @{Tooltip.Show} when the user hovers over the element, or nil to clear it
-- @treturn Element The element object
function Element.SetTooltip(self, tooltip)
self._tooltip = tooltip
if tooltip then
-- setting OnEnter/OnLeave will implicitly enable the mouse, so make sure it's previously been enabled
assert(self:_GetBaseFrame():IsMouseEnabled())
self:SetScript("OnEnter", private.OnEnter)
self:SetScript("OnLeave", private.OnLeave)
else
self:SetScript("OnEnter", nil)
self:SetScript("OnLeave", nil)
end
return self
end
--- Shows a tooltip on the element.
-- @tparam Element self The element object
-- @param tooltip The value passed to @{Tooltip.Show} when the user hovers over the element
-- @tparam ?boolean noWrapping Disables wrapping of text lines
-- @tparam[opt=0] number xOffset An extra x offset to apply to the anchor of the tooltip
-- @treturn Element The element object
function Element.ShowTooltip(self, tooltip, noWrapping, xOffset)
Tooltip.Show(self:_GetBaseFrame(), tooltip, noWrapping, xOffset)
return self
end
--- Sets the context value of the element.
-- @tparam Element self The element object
-- @param context The context value
-- @treturn Element The element object
function Element.SetContext(self, context)
self._context = context
return self
end
--- Gets the context value from the element.
-- @tparam Element self The element object
-- @return The context value
function Element.GetContext(self)
return self._context
end
--- Registers a script handler.
-- @tparam Element self The element object
-- @tparam string script The script to register for
-- @tparam function handler The script handler which will be called with the element object followed by any arguments to
-- the script
-- @treturn Element The element object
function Element.SetScript(self, script, handler)
self._scripts[script] = handler
if handler then
ScriptWrapper.Set(self:_GetBaseFrame(), script, handler, self)
else
ScriptWrapper.Clear(self:_GetBaseFrame(), script)
end
return self
end
--- Sets a script to propagate to the parent element.
-- @tparam Element self The element object
-- @tparam string script The script to propagate
-- @treturn Element The element object
function Element.PropagateScript(self, script)
self._scripts[script] = "__PROPAGATE"
ScriptWrapper.SetPropagate(self:_GetBaseFrame(), script, self)
return self
end
function Element.Draw(self)
assert(self._acquired)
local frame = self:_GetBaseFrame()
local numAnchors = self:_GetNumAnchors()
if numAnchors > 0 then
frame:ClearAllPoints()
for i = 1, numAnchors do
local point, relFrame, relPoint, x, y = self:_GetAnchor(i)
if relFrame == ANCHOR_REL_PARENT then
relFrame = frame:GetParent()
elseif type(relFrame) == "string" then
-- this is a relative element
relFrame = self:GetParentElement():GetElement(relFrame):_GetBaseFrame()
end
frame:SetPoint(point, relFrame, relPoint, x, y)
end
end
local width = self._width
if width then
self:_SetDimension("WIDTH", width)
end
local height = self._height
if height then
self:_SetDimension("HEIGHT", height)
end
local relativeLevel = self._relativeLevel
if relativeLevel then
frame:SetFrameLevel(frame:GetParent():GetFrameLevel() + relativeLevel)
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Element._GetNumAnchors(self)
assert(#self._anchors % 5 == 0)
return #self._anchors / 5
end
function Element._GetAnchor(self, index)
index = (index - 1) * 5 + 1
assert(index < #self._anchors)
return unpack(self._anchors, index, index + 4)
end
function Element._SetParentElement(self, parent)
self._parent = parent
self:_ClearBaseElementCache()
end
function Element._ClearBaseElementCache(self)
self._baseElementCache = nil
end
function Element._GetMinimumDimension(self, dimension)
if dimension == "WIDTH" then
local width = self._width
return width or 0, width == nil
elseif dimension == "HEIGHT" then
local height = self._height
return height or 0, height == nil
else
error("Invalid dimension: " .. tostring(dimension))
end
end
function Element._GetPreferredDimension(self, dimension)
if dimension == "WIDTH" then
return nil
elseif dimension == "HEIGHT" then
return nil
else
error("Invalid dimension: " .. tostring(dimension))
end
end
function Element._GetDimension(self, dimension)
if dimension == "WIDTH" then
return self:_GetBaseFrame():GetWidth()
elseif dimension == "HEIGHT" then
return self:_GetBaseFrame():GetHeight()
else
error("Invalid dimension: " .. tostring(dimension))
end
end
function Element._SetDimension(self, dimension, ...)
if dimension == "WIDTH" then
self:_GetBaseFrame():SetWidth(...)
elseif dimension == "HEIGHT" then
self:_GetBaseFrame():SetHeight(...)
else
error("Invalid dimension: " .. tostring(dimension))
end
end
function Element._GetBaseFrame(self)
return self._frame
end
function Element._GetPadding(self, side)
return self._padding[strlower(side)]
end
function Element._GetPaddingAnchorOffsets(self, anchor)
local xPart, yPart = private.SplitAnchor(anchor)
local x = xPart and ((xPart == "LEFT" and 1 or -1) * self:_GetPadding(xPart)) or 0
local y = yPart and ((yPart == "BOTTOM" and 1 or -1) * self:_GetPadding(yPart)) or 0
return x, y
end
function Element._GetMargin(self, side)
return self._margin[strlower(side)]
end
function Element._GetMarginAnchorOffsets(self, anchor)
local xPart, yPart = private.SplitAnchor(anchor)
local x = xPart and ((xPart == "LEFT" and 1 or -1) * self:_GetMargin(xPart)) or 0
local y = yPart and ((yPart == "BOTTOM" and 1 or -1) * self:_GetMargin(yPart)) or 0
return x, y
end
-- ============================================================================
-- Helper Functions
-- ============================================================================
function private.GetElementHelper(element, path)
local numParts = select("#", strsplit(".", path))
local partIndex = 1
while partIndex <= numParts do
local part = select(partIndex, strsplit(".", path))
if part == "__parent" then
local parentElement = element:GetParentElement()
if not parentElement then
error(format("Element (%s) has no parent", tostring(element._id)))
end
element = parentElement
elseif part == "__base" then
local baseElement = element:GetBaseElement()
if not baseElement then
error(format("Element (%s) has no base element", tostring(element._id)))
end
element = baseElement
else
local found = false
for _, child in ipairs(element._children) do
if child._id == part then
element = child
found = true
break
end
end
if not found then
element = nil
break
end
end
partIndex = partIndex + 1
end
return element
end
function private.SplitAnchor(anchor)
if anchor == "BOTTOMLEFT" then
return "LEFT", "BOTTOM"
elseif anchor == "BOTTOM" then
return nil, "BOTTOM"
elseif anchor == "BOTTOMRIGHT" then
return "RIGHT", "BOTTOM"
elseif anchor == "RIGHT" then
return "RIGHT", nil
elseif anchor == "TOPRIGHT" then
return "RIGHT", "TOP"
elseif anchor == "TOP" then
return nil, "TOP"
elseif anchor == "TOPLEFT" then
return "LEFT", "TOP"
elseif anchor == "LEFT" then
return "LEFT", nil
else
error("Invalid anchor: "..tostring(anchor))
end
end
function private.OnEnter(element)
element:ShowTooltip(element._tooltip)
end
function private.OnLeave(element)
Tooltip.Hide()
end

456
Core/UI/Elements/Frame.lua Normal file
View File

@@ -0,0 +1,456 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Frame UI Element Class.
-- A frame is a container which supports automated layout of its children. It also supports being the base element of a UI and anchoring/parenting directly to a WoW frame. It is a subclass of the @{Container} class.
-- @classmod Frame
local _, TSM = ...
local TempTable = TSM.Include("Util.TempTable")
local Table = TSM.Include("Util.Table")
local Color = TSM.Include("Util.Color")
local Theme = TSM.Include("Util.Theme")
local NineSlice = TSM.Include("Util.NineSlice")
local VALID_LAYOUTS = {
NONE = true,
HORIZONTAL = true,
VERTICAL = true,
FLOW = true,
}
local LAYOUT_CONTEXT = {
VERTICAL = {
primaryDimension = "HEIGHT",
secondaryDimension = "WIDTH",
sides = { primary = { "TOP", "BOTTOM" }, secondary = { "LEFT", "RIGHT" } },
},
HORIZONTAL = {
primaryDimension = "WIDTH",
secondaryDimension = "HEIGHT",
sides = { primary = { "LEFT", "RIGHT" }, secondary = { "TOP", "BOTTOM" } },
},
}
local Frame = TSM.Include("LibTSMClass").DefineClass("Frame", TSM.UI.Container)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Frame)
TSM.UI.Frame = Frame
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Frame.__init(self)
local frame = UIElements.CreateFrame(self, "Frame")
self.__super:__init(frame)
self._borderNineSlice = NineSlice.New(frame)
self._borderNineSlice:Hide()
self._backgroundNineSlice = NineSlice.New(frame, 1)
self._backgroundNineSlice:Hide()
self._layout = "NONE"
self._backgroundColor = nil
self._roundedCorners = false
self._borderColor = nil
self._borderSize = nil
self._expandWidth = false
self._strata = nil
self._scale = 1
end
function Frame.Release(self)
self._layout = "NONE"
self._backgroundColor = nil
self._roundedCorners = false
self._borderColor = nil
self._borderSize = nil
self._expandWidth = false
self._strata = nil
self._scale = 1
self._borderNineSlice:Hide()
self._backgroundNineSlice:Hide()
local frame = self:_GetBaseFrame()
frame:RegisterForDrag(nil)
frame:EnableMouse(false)
frame:SetMovable(false)
frame:EnableMouseWheel(false)
frame:SetHitRectInsets(0, 0, 0, 0)
self.__super:Release()
end
--- Sets the background of the frame.
-- @tparam Frame self The frame object
-- @tparam ?string|Color|nil color The background color as a theme color key, Color object, or nil
-- @tparam[opt=false] boolean roundedCorners Whether or not the corners should be rounded
-- @treturn Frame The frame object
function Frame.SetBackgroundColor(self, color, roundedCorners)
assert(color == nil or Color.IsInstance(color) or Theme.GetColor(color))
self._backgroundColor = color
self._roundedCorners = roundedCorners
return self
end
--- Sets the border color of the frame.
-- @tparam Frame self The frame object
-- @tparam ?string|nil color The border color as a theme color key or nil
-- @tparam[opt=1] ?number borderSize The border size
-- @treturn Frame The frame object
function Frame.SetBorderColor(self, color, borderSize)
assert(color == nil or Color.IsInstance(color) or Theme.GetColor(color))
self._borderColor = color
self._borderSize = borderSize or 1
return self
end
--- Sets the width of the frame.
-- @tparam Frame self The frame object
-- @tparam ?number|string width The width of the frame, "EXPAND" to set the width to expand to be
-- as large as possible, or nil to have an undefined width
-- @treturn Frame The frame object
function Frame.SetWidth(self, width)
if width == "EXPAND" then
self._expandWidth = true
else
self.__super:SetWidth(width)
end
return self
end
--- Sets the parent frame.
-- @tparam Frame self The frame object
-- @tparam frame parent The WoW frame to parent to
-- @treturn Frame The frame object
function Frame.SetParent(self, parent)
self:_GetBaseFrame():SetParent(parent)
return self
end
--- Sets the level of the frame.
-- @tparam Frame self The frame object
-- @tparam number level The frame level
-- @treturn Frame The frame object
function Frame.SetFrameLevel(self, level)
self:_GetBaseFrame():SetFrameLevel(level)
return self
end
--- Sets the strata of the frame.
-- @tparam Frame self The frame object
-- @tparam string strata The frame strata
-- @treturn Frame The frame object
function Frame.SetStrata(self, strata)
self._strata = strata
return self
end
--- Sets the scale of the frame.
-- @tparam Frame self The frame object
-- @tparam string scale The frame scale
-- @treturn Frame The frame object
function Frame.SetScale(self, scale)
self._scale = scale
return self
end
--- Sets the layout of the frame.
-- @tparam Frame self The frame object
-- @tparam string layout The frame layout (`NONE`, `HORIZONTAL`, `VERTICAL`, or `FLOW`)
-- @treturn Frame The frame object
function Frame.SetLayout(self, layout)
assert(VALID_LAYOUTS[layout], format("Invalid layout (%s)", tostring(layout)))
self._layout = layout
return self
end
--- Sets whether mouse interaction is enabled.
-- @tparam Frame self The frame object
-- @tparam boolean enabled Whether mouse interaction is enabled
-- @treturn Frame The frame object
function Frame.SetMouseEnabled(self, enabled)
self:_GetBaseFrame():EnableMouse(enabled)
return self
end
--- Sets whether mouse wheel interaction is enabled.
-- @tparam Frame self The frame object
-- @tparam boolean enabled Whether mouse wheel interaction is enabled
-- @treturn Frame The frame object
function Frame.SetMouseWheelEnabled(self, enabled)
self:_GetBaseFrame():EnableMouseWheel(enabled)
return self
end
--- Allows dragging of the frame.
-- @tparam Frame self The frame object
-- @tparam string button The button to support dragging with
-- @treturn Frame The frame object
function Frame.RegisterForDrag(self, button)
self:SetMouseEnabled(button and true or false)
self:_GetBaseFrame():RegisterForDrag(button)
return self
end
--- Gets whether the mouse is currently over the frame.
-- @tparam Frame self The frame object
-- @treturn boolean Whether or not the mouse is over the frame
function Frame.IsMouseOver(self)
return self:_GetBaseFrame():IsMouseOver()
end
--- Sets the hit rectangle insets.
-- @tparam Frame self The frame object
-- @tparam number left The left hit rectangle inset
-- @tparam number right The right hit rectangle inset
-- @tparam number top The top hit rectangle inset
-- @tparam number bottom The bottom hit rectangle inset
-- @treturn Frame The frame object
function Frame.SetHitRectInsets(self, left, right, top, bottom)
self:_GetBaseFrame():SetHitRectInsets(left, right, top, bottom)
return self
end
--- Makes the element movable and starts moving it.
-- @tparam Frame self The element object
function Frame.StartMoving(self)
self:_GetBaseFrame():SetMovable(true)
self:_GetBaseFrame():StartMoving()
return self
end
--- Stops moving the element, and makes it unmovable.
-- @tparam Frame self The element object
function Frame.StopMovingOrSizing(self)
self:_GetBaseFrame():StopMovingOrSizing()
self:_GetBaseFrame():SetMovable(false)
return self
end
function Frame.Draw(self)
local layout = self._layout
self.__super.__super:Draw()
local frame = self:_GetBaseFrame()
if self._backgroundColor then
self._backgroundNineSlice:SetStyle(self._roundedCorners and "rounded" or "solid", self._borderColor and self._borderSize or nil)
local color = Color.IsInstance(self._backgroundColor) and self._backgroundColor or Theme.GetColor(self._backgroundColor)
self._backgroundNineSlice:SetVertexColor(color:GetFractionalRGBA())
else
assert(not self._borderColor)
self._backgroundNineSlice:Hide()
end
if self._borderColor then
assert(self._backgroundColor)
self._borderNineSlice:SetStyle(self._roundedCorners and "rounded" or "solid")
local color = Color.IsInstance(self._borderColor) and self._borderColor or Theme.GetColor(self._borderColor)
self._borderNineSlice:SetVertexColor(color:GetFractionalRGBA())
else
self._borderNineSlice:Hide()
end
frame:SetScale(self._scale)
local strata = self._strata
if strata then
frame:SetFrameStrata(strata)
end
if layout == "NONE" then
-- pass
elseif layout == "FLOW" then
local width = self:_GetDimension("WIDTH")
local height = self:_GetDimension("HEIGHT") - self:_GetPadding("TOP") - self:_GetPadding("BOTTOM")
local rowHeight = 0
for _, child in self:LayoutChildrenIterator() do
child:_GetBaseFrame():ClearAllPoints()
local childPrimary = child:_GetMinimumDimension("WIDTH")
child:_SetDimension("WIDTH", childPrimary)
local childSecondary = child:_GetMinimumDimension("HEIGHT")
rowHeight = childSecondary + child:_GetMargin("BOTTOM") + child:_GetMargin("TOP")
child:_SetDimension("HEIGHT", childSecondary)
end
local xOffset = self:_GetPadding("LEFT")
-- calculate the Y offset to properly position stuff with the padding of this frame taken into account
local yOffset = -self:_GetPadding("TOP")
for _, child in self:LayoutChildrenIterator() do
local childFrame = child:_GetBaseFrame()
local childWidth = childFrame:GetWidth() + child:_GetMargin("LEFT") + child:_GetMargin("RIGHT")
if xOffset + childWidth + self:_GetPadding("RIGHT") > width then
-- move to the next row
xOffset = self:_GetPadding("LEFT")
yOffset = yOffset - rowHeight
end
local childYOffset = yOffset + (height - childFrame:GetHeight()) / 2 - child:_GetMargin("TOP")
childFrame:SetPoint("LEFT", xOffset + child:_GetMargin("LEFT"), childYOffset)
xOffset = xOffset + childWidth
end
else
local context = LAYOUT_CONTEXT[layout]
assert(context)
local primary = self:_GetDimension(context.primaryDimension) - self:_GetPadding(context.sides.primary[1]) - self:_GetPadding(context.sides.primary[2])
local secondary = self:_GetDimension(context.secondaryDimension) - self:_GetPadding(context.sides.secondary[1]) - self:_GetPadding(context.sides.secondary[2])
local expandChildren = TempTable.Acquire()
local preferredChildren = TempTable.Acquire()
for _, child in self:LayoutChildrenIterator() do
child:_GetBaseFrame():ClearAllPoints()
local childPrimary, childPrimaryCanExpand = child:_GetMinimumDimension(context.primaryDimension)
if childPrimaryCanExpand then
local childPreferredPrimary = child:_GetPreferredDimension(context.primaryDimension)
if childPreferredPrimary then
assert(childPreferredPrimary > childPrimary, "Invalid preferred dimension")
preferredChildren[child] = childPreferredPrimary
else
expandChildren[child] = childPrimary
end
else
child:_SetDimension(context.primaryDimension, childPrimary)
end
primary = primary - childPrimary - child:_GetMargin(context.sides.primary[1]) - child:_GetMargin(context.sides.primary[2])
local childSecondary, childSecondaryCanExpand = child:_GetMinimumDimension(context.secondaryDimension)
childSecondary = min(childSecondary, secondary)
if childSecondaryCanExpand and childSecondary < secondary then
childSecondary = secondary
end
child:_SetDimension(context.secondaryDimension, childSecondary - child:_GetMargin(context.sides.secondary[1]) - child:_GetMargin(context.sides.secondary[2]))
end
for child, preferredPrimary in pairs(preferredChildren) do
local childPrimary = min(primary, preferredPrimary)
child:_SetDimension(context.primaryDimension, childPrimary)
primary = primary - (childPrimary - child:_GetMinimumDimension(context.primaryDimension))
end
local numExpandChildren = Table.Count(expandChildren)
for child, childPrimary in pairs(expandChildren) do
childPrimary = max(childPrimary, childPrimary + primary / numExpandChildren)
child:_SetDimension(context.primaryDimension, childPrimary)
end
TempTable.Release(expandChildren)
TempTable.Release(preferredChildren)
if layout == "HORIZONTAL" then
local xOffset = self:_GetPadding("LEFT")
-- calculate the Y offset to properly position stuff with the padding of this frame taken into account
local yOffset = (self:_GetPadding("BOTTOM") - self:_GetPadding("TOP")) / 2
for _, child in self:LayoutChildrenIterator() do
local childFrame = child:_GetBaseFrame()
xOffset = xOffset + child:_GetMargin("LEFT")
local childYOffset = (child:_GetMargin("BOTTOM") - child:_GetMargin("TOP")) / 2
childFrame:SetPoint("LEFT", xOffset, childYOffset + yOffset)
xOffset = xOffset + childFrame:GetWidth() + child:_GetMargin("RIGHT")
end
elseif layout == "VERTICAL" then
local yOffset = -self:_GetPadding("TOP")
-- calculate the X offset to properly position stuff with the padding of this frame taken into account
local xOffset = (self:_GetPadding("LEFT") - self:_GetPadding("RIGHT")) / 2
for _, child in self:LayoutChildrenIterator() do
local childFrame = child:_GetBaseFrame()
yOffset = yOffset - child:_GetMargin("TOP")
local childXOffset = (child:_GetMargin("LEFT") - child:_GetMargin("RIGHT")) / 2
childFrame:SetPoint("TOP", childXOffset + xOffset, yOffset)
yOffset = yOffset - childFrame:GetHeight() - child:_GetMargin("BOTTOM")
end
else
error()
end
end
for _, child in self:LayoutChildrenIterator() do
child:Draw()
end
for _, child in ipairs(self._noLayoutChildren) do
child:Draw()
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Frame._GetMinimumDimension(self, dimension)
assert(dimension == "WIDTH" or dimension == "HEIGHT")
local styleResult = nil
if dimension == "WIDTH" then
styleResult = self._width
elseif dimension == "HEIGHT" then
styleResult = self._height
else
error("Invalid dimension: "..tostring(dimension))
end
local layout = self._layout
local context = LAYOUT_CONTEXT[layout]
if styleResult then
return styleResult, false
elseif self:GetNumLayoutChildren() == 0 or layout == "NONE" then
return 0, true
elseif layout == "FLOW" then
-- calculate our minimum width which is the largest of the widths of the children
local minWidth = 0
for _, child in self:LayoutChildrenIterator() do
local childMin = child:_GetMinimumDimension("WIDTH")
childMin = childMin + child:_GetMargin("LEFT") + child:_GetMargin("RIGHT")
minWidth = max(minWidth, childMin)
end
minWidth = minWidth + self:_GetPadding("LEFT") + self:_GetPadding("RIGHT")
if dimension == "WIDTH" then
return minWidth, true
end
-- calculate the row height (all children should be the exact same height)
local rowHeight = nil
for _, child in self:LayoutChildrenIterator() do
local childMin, childCanExpand = child:_GetMinimumDimension("HEIGHT")
childMin = childMin + child:_GetMargin("TOP") + child:_GetMargin("BOTTOM")
rowHeight = rowHeight or childMin
assert(childMin == rowHeight and not childCanExpand)
end
rowHeight = rowHeight or 0
local parentElement = self:GetParentElement()
local parentWidth = parentElement:_GetDimension("WIDTH") - parentElement:_GetPadding("LEFT") - parentElement:_GetPadding("RIGHT")
if minWidth > parentWidth then
-- we won't fit, so just pretend we're a single row
return rowHeight, false
end
-- calculate our height based on our parent's width
local height = rowHeight
local currentRowWidth = 0
for _, child in self:LayoutChildrenIterator() do
local childWidth = child:_GetMinimumDimension("WIDTH") + child:_GetMargin("LEFT") + child:_GetMargin("RIGHT")
if currentRowWidth + childWidth > parentWidth then
-- this child will go on the next row
height = height + rowHeight
currentRowWidth = childWidth
else
-- this child fits on the current row
currentRowWidth = currentRowWidth + childWidth
end
end
return height, false
elseif context then
-- calculate the dimension based on the children
local sides = (dimension == context.primaryDimension) and context.sides.primary or context.sides.secondary
local result = 0
local canExpand = false
for _, child in self:LayoutChildrenIterator() do
local childMin, childCanExpand = child:_GetMinimumDimension(dimension)
childMin = childMin + child:_GetMargin(sides[1]) + child:_GetMargin(sides[2])
canExpand = canExpand or childCanExpand
if dimension == context.primaryDimension then
result = result + childMin
else
result = max(result, childMin)
end
end
result = result + self:_GetPadding(sides[1]) + self:_GetPadding(sides[2])
return result, self._expandWidth or canExpand
else
error(format("Invalid layout (%s)", tostring(layout)))
end
end

615
Core/UI/Elements/Graph.lua Normal file
View File

@@ -0,0 +1,615 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Graph UI Element Class.
-- The graph element allows for generating line graphs. It is a subclass of the @{Element} class.
-- @classmod Graph
local _, TSM = ...
local Math = TSM.Include("Util.Math")
local Theme = TSM.Include("Util.Theme")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Graph = TSM.Include("LibTSMClass").DefineClass("Graph", TSM.UI.Element)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Graph)
TSM.UI.Graph = Graph
local private = {}
local PLOT_X_LABEL_WIDTH = 48
local PLOT_X_LABEL_HEIGHT = 16
local PLOT_X_LABEL_MARGIN = 6
local PLOT_Y_LABEL_WIDTH = 48
local PLOT_Y_LABEL_HEIGHT = 16
local PLOT_Y_LABEL_MARGIN = 4
local PLOT_HIGHLIGHT_TEXT_WIDTH = 80
local PLOT_HIGHLIGHT_TEXT_HEIGHT = 16
local PLOT_X_EXTRA_HIT_RECT = 4
local PLOT_Y_MARGIN = 4
local LINE_THICKNESS = 1
local LINE_THICKNESS_RATIO = 16
local PLOT_MIN_X_LINE_SPACING = PLOT_X_LABEL_WIDTH * 1.5 + 8
local PLOT_MIN_Y_LINE_SPACING = PLOT_Y_LABEL_HEIGHT * 1.5 + 8
local HOVER_LINE_THICKNESS = 1
local MAX_FILL_ALPHA = 0.5
local SELECTION_ALPHA = 0.2
local MAX_PLOT_POINTS = 300
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Graph.__init(self)
local frame = UIElements.CreateFrame(self, "Frame", nil, nil, TSM.IsShadowlands() and "BackdropTemplate" or nil)
self.__super:__init(frame)
frame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
frame.plot = CreateFrame("Frame", nil, frame, nil)
frame.plot:SetPoint("BOTTOMLEFT", PLOT_Y_LABEL_WIDTH, PLOT_X_LABEL_HEIGHT)
frame.plot:SetPoint("TOPRIGHT", -PLOT_X_EXTRA_HIT_RECT, -PLOT_HIGHLIGHT_TEXT_HEIGHT - PLOT_Y_MARGIN)
frame.plot:SetHitRectInsets(-PLOT_X_EXTRA_HIT_RECT, -PLOT_X_EXTRA_HIT_RECT, 0, 0)
frame.plot:EnableMouse(true)
ScriptWrapper.Set(frame.plot, "OnEnter", private.PlotFrameOnEnter, self)
ScriptWrapper.Set(frame.plot, "OnLeave", private.PlotFrameOnLeave, self)
ScriptWrapper.Set(frame.plot, "OnMouseDown", private.PlotFrameOnMouseDown, self)
ScriptWrapper.Set(frame.plot, "OnMouseUp", private.PlotFrameOnMouseUp, self)
frame.plot.dot = frame.plot:CreateTexture(nil, "ARTWORK", nil, 3)
TSM.UI.TexturePacks.SetTextureAndSize(frame.plot.dot, "uiFrames.HighlightDot")
frame.plot.hoverLine = frame.plot:CreateTexture(nil, "ARTWORK", nil, 2)
frame.plot.hoverLine:SetWidth(HOVER_LINE_THICKNESS)
frame.plot.hoverLine:Hide()
frame.plot.hoverText = frame.plot:CreateFontString()
frame.plot.hoverText:SetSize(PLOT_HIGHLIGHT_TEXT_WIDTH, PLOT_HIGHLIGHT_TEXT_HEIGHT)
frame.plot.hoverText:Hide()
frame.plot.selectionBox = frame.plot:CreateTexture(nil, "ARTWORK", nil, 2)
frame.plot.selectionBox:Hide()
self._usedTextures = {}
self._freeTextures = {}
self._usedFontStrings = {}
self._freeFontStrings = {}
self._xValuesFiltered = {}
self._yLookup = {}
self._yValueFunc = nil
self._xFormatFunc = nil
self._yFormatFunc = nil
self._xStepFunc = nil
self._yStepFunc = nil
self._xMin = nil
self._xMax = nil
self._yMin = nil
self._yMax = nil
self._isMouseOver = false
self._selectionStartX = nil
self._zoomStart = nil
self._zoomEnd = nil
self._onZoomChanged = nil
self._onHoverUpdate = nil
end
function Graph.Release(self)
self:_ReleaseAllTextures()
self:_ReleaseAllFontStrings()
wipe(self._xValuesFiltered)
wipe(self._yLookup)
self._yValueFunc = nil
self._xFormatFunc = nil
self._yFormatFunc = nil
self._xStepFunc = nil
self._yStepFunc = nil
self._xMin = nil
self._xMax = nil
self._yMin = nil
self._yMax = nil
self._isMouseOver = false
self._selectionStartX = nil
self._zoomStart = nil
self._zoomEnd = nil
self._onZoomChanged = nil
self._onHoverUpdate = nil
self.__super:Release()
end
--- Sets the step size of the axes.
-- @tparam Graph self The graph object
-- @tparam function x A function which gets the next x-axis step value
-- @tparam function y A function which gets the next y-axis step value
-- @treturn Graph The graph object
function Graph.SetAxisStepFunctions(self, x, y)
self._xStepFunc = x
self._yStepFunc = y
return self
end
function Graph.SetXRange(self, xMin, xMax, stepInterval)
assert(xMin <= xMax)
self._xMin = xMin
self._xMax = xMax
self._xStepInterval = stepInterval
self._zoomStart = xMin
self._zoomEnd = xMax
return self
end
function Graph.SetZoom(self, zoomStart, zoomEnd)
self._zoomStart = zoomStart
self._zoomEnd = zoomEnd
return self
end
function Graph.GetZoom(self)
return self._zoomStart, self._zoomEnd
end
function Graph.GetXRange(self)
local yMin, yMax = nil, nil
for _, x in ipairs(self._xValuesFiltered) do
local y = self._yValueFunc(x)
yMin = min(yMin or math.huge, y)
yMax = max(yMax or -math.huge, y)
end
return self._xMin, self._xMax
end
function Graph.GetYRange(self)
local yMin, yMax = nil, nil
for _, x in ipairs(self._xValuesFiltered) do
local y = self._yValueFunc(x)
yMin = min(yMin or math.huge, y)
yMax = max(yMax or -math.huge, y)
end
return yMin, yMax
end
function Graph.SetYValueFunction(self, func)
self._yValueFunc = func
return self
end
--- Sets functions for formatting values.
-- @tparam Graph self The graph object
-- @tparam function xFormatFunc A function which is passed an x value and returns a formatted string
-- @tparam function yFormatFunc A function which is passed a y value and returns a formatted string
-- @treturn Graph The graph object
function Graph.SetFormatFunctions(self, xFormatFunc, yFormatFunc)
self._xFormatFunc = xFormatFunc
self._yFormatFunc = yFormatFunc
return self
end
--- Registers a script handler.
-- @tparam ScrollingTable self The graph object
-- @tparam string script The script to register for (supported scripts: `OnZoomChanged`)
-- @tparam function handler The script handler which will be called with the graph object followed by any
-- arguments to the script
-- @treturn Graph The graph object
function Graph.SetScript(self, script, handler)
if script == "OnZoomChanged" then
self._onZoomChanged = handler
elseif script == "OnHoverUpdate" then
self._onHoverUpdate = handler
else
error("Unknown Graph script: "..tostring(script))
end
return self
end
function Graph.Draw(self)
self.__super:Draw()
self:_ReleaseAllTextures()
self:_ReleaseAllFontStrings()
local frame = self:_GetBaseFrame()
frame:SetBackdropColor(Theme.GetColor("PRIMARY_BG"):GetFractionalRGBA())
local plot = frame.plot
plot.hoverText:SetFont(Theme.GetFont("TABLE_TABLE1"):GetWowFont())
plot.hoverText:SetTextColor(Theme.GetColor("INDICATOR_ALT"):GetFractionalRGBA())
local plotWidth = plot:GetWidth()
local plotHeight = plot:GetHeight()
-- update the filtered set of x values to show and the bounds of the plot data
self:_PopulateFilteredData(plotWidth)
-- calculate the min and max y values which should be shown
self._yMin, self._yMax = self._yStepFunc("RANGE", self._yMin, self._yMax, floor(plotHeight / PLOT_MIN_Y_LINE_SPACING))
if Math.IsNan(self._yMax) then
-- this happens when we're resizing the application frame
return
end
-- draw the y axis lines and labels
local prevYAxisOffset = -math.huge
local yAxisValue = self._yMin
while yAxisValue <= self._yMax do
local yAxisOffset = Math.Scale(yAxisValue, self._yMin, self._yMax, 0, plotHeight)
if not prevYAxisOffset or (yAxisOffset - prevYAxisOffset) >= PLOT_MIN_Y_LINE_SPACING then
self:_DrawYAxisLine(yAxisOffset, yAxisValue, plotWidth, plotHeight)
prevYAxisOffset = yAxisOffset
end
yAxisValue = self._yStepFunc("NEXT", yAxisValue, self._yMax)
end
-- draw the x axis lines and labels
local xSuggestedStep = Math.Scale(PLOT_MIN_X_LINE_SPACING, 0, plotWidth, 0, self._zoomEnd - self._zoomStart)
local prevXAxisOffset = -math.huge
local xAxisValue = self._xStepFunc(self._zoomStart, xSuggestedStep)
while xAxisValue <= self._zoomEnd do
local xAxisOffset = Math.Scale(xAxisValue, self._zoomStart, self._zoomEnd, 0, plotWidth)
if not prevXAxisOffset or (xAxisOffset - prevXAxisOffset) > PLOT_MIN_X_LINE_SPACING then
self:_DrawXAxisLine(xAxisOffset, xAxisValue, plotWidth, plotHeight, xSuggestedStep)
prevXAxisOffset = xAxisOffset
end
xAxisValue = self._xStepFunc(xAxisValue, xSuggestedStep)
end
-- draw all the lines
local color = nil
if self._isMouseOver or self._selectionStartX then
color = Theme.GetColor("INDICATOR_ALT")
elseif self._yLookup[self._xValuesFiltered[1]] <= self._yLookup[self._xValuesFiltered[#self._xValuesFiltered]] then
color = Theme.GetFeedbackColor("GREEN")
else
color = Theme.GetFeedbackColor("RED")
end
local xPrev, yPrev = nil, nil
for _, x in ipairs(self._xValuesFiltered) do
local y = self._yLookup[x]
local xCoord = Math.Scale(x, self._zoomStart, self._zoomEnd, 0, plotWidth)
local yCoord = Math.Scale(y, self._yMin, self._yMax, 0, plotHeight)
if xPrev then
self:_DrawFillLine(xPrev, yPrev, xCoord, yCoord, LINE_THICKNESS, plotHeight, color)
end
xPrev = xCoord
yPrev = yCoord
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Graph._PopulateFilteredData(self, plotWidth)
wipe(self._xValuesFiltered)
wipe(self._yLookup)
self._yMin = math.huge
self._yMax = -math.huge
local minStep = Math.Ceil((self._zoomEnd - self._zoomStart) / min(plotWidth / 3, MAX_PLOT_POINTS), self._xStepInterval)
local x = self._zoomStart
while x <= self._zoomEnd do
local prevX = self._xValuesFiltered[#self._xValuesFiltered]
if not prevX or x == self._zoomEnd or (x - prevX > minStep and self._zoomEnd - x > minStep) then
-- this is either the first / last point or a middle point which is sufficiently far from the previous and last points
tinsert(self._xValuesFiltered, x)
local y = self._yValueFunc(x)
self._yMin = min(self._yMin, y)
self._yMax = max(self._yMax, y)
self._yLookup[x] = y
end
if x == self._zoomEnd then
break
end
x = min(x + minStep, self._zoomEnd)
end
end
function Graph._DrawYAxisLine(self, yOffset, yValue, plotWidth, plotHeight, ySuggestedStep)
local line = self:_AcquireLine("ARTWORK")
local thickness = LINE_THICKNESS
local textureHeight = thickness * LINE_THICKNESS_RATIO
-- trim the texture a bit on the left/right since it's not completely filled to the edges which is noticeable on long lines
line:SetTexCoord(0.1, 1, 0.1, 0, 0.9, 1, 0.9, 0)
line:SetPoint("BOTTOMLEFT", 0 - thickness / 2, yOffset - textureHeight / 2)
line:SetPoint("TOPRIGHT", line:GetParent(), "BOTTOMLEFT", plotWidth + thickness / 2, yOffset + textureHeight / 2)
line:SetVertexColor(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
line:SetDrawLayer("BACKGROUND", 0)
local text = self:_AcquireFontString(Theme.GetFont("TABLE_TABLE1"))
text:SetJustifyH("RIGHT")
local textYOffset = 0
if PLOT_Y_LABEL_HEIGHT / 2 > yOffset then
text:SetJustifyV("BOTTOM")
textYOffset = max(PLOT_Y_LABEL_HEIGHT / 2 - yOffset, 0)
elseif yOffset + PLOT_Y_LABEL_HEIGHT / 2 > plotHeight then
text:SetJustifyV("TOP")
textYOffset = plotHeight - yOffset - PLOT_Y_LABEL_HEIGHT / 2
else
text:SetJustifyV("MIDDLE")
end
text:SetPoint("RIGHT", line, "LEFT", -PLOT_Y_LABEL_MARGIN, textYOffset)
text:SetSize(PLOT_Y_LABEL_WIDTH, PLOT_Y_LABEL_HEIGHT)
text:SetText(self._yFormatFunc(yValue, ySuggestedStep))
end
function Graph._DrawXAxisLine(self, xOffset, xValue, plotWidth, plotHeight, xSuggestedStep)
local line = self:_AcquireLine("ARTWORK")
local thickness = LINE_THICKNESS
local textureHeight = thickness * LINE_THICKNESS_RATIO
-- trim the texture a bit on the left/right since it's not completely filled to the edges which is noticeable on long lines
line:SetTexCoord(0.9, 1, 0.1, 1, 0.9, 0, 0.1, 0)
line:SetPoint("BOTTOMLEFT", xOffset - textureHeight / 2, thickness / 2)
line:SetPoint("TOPRIGHT", line:GetParent(), "BOTTOMLEFT", xOffset + textureHeight / 2, plotHeight + thickness / 2)
line:SetVertexColor(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
line:SetDrawLayer("BACKGROUND", 0)
local text = self:_AcquireFontString(Theme.GetFont("BODY_BODY3_MEDIUM"))
text:ClearAllPoints()
text:SetJustifyV("TOP")
local textXOffset = 0
if PLOT_X_LABEL_WIDTH / 2 > xOffset then
text:SetJustifyH("LEFT")
textXOffset = max(PLOT_X_LABEL_WIDTH / 2 - xOffset, 0)
elseif xOffset + PLOT_X_LABEL_WIDTH / 2 > plotWidth then
text:SetJustifyH("RIGHT")
textXOffset = plotWidth - xOffset - PLOT_X_LABEL_WIDTH / 2
else
text:SetJustifyH("CENTER")
end
text:SetPoint("TOP", line, "BOTTOM", textXOffset, -PLOT_X_LABEL_MARGIN)
text:SetSize(PLOT_X_LABEL_WIDTH, PLOT_X_LABEL_HEIGHT)
text:SetText(self._xFormatFunc(xValue, xSuggestedStep))
end
function Graph._DrawFillLine(self, xFrom, yFrom, xTo, yTo, thickness, plotHeight, color)
assert(xFrom <= xTo)
local line = self:_AcquireLine("ARTWORK")
local textureHeight = thickness * LINE_THICKNESS_RATIO
local xDiff = xTo - xFrom
local yDiff = yTo - yFrom
local length = sqrt(xDiff * xDiff + yDiff * yDiff)
local sinValue = -yDiff / length
local cosValue = xDiff / length
local sinCosValue = sinValue * cosValue
local aspectRatio = length / textureHeight
local invAspectRatio = textureHeight / length
-- calculate and set tex coords
local LLx, LLy, ULx, ULy, URx, URy, LRx, LRy = nil, nil, nil, nil, nil, nil, nil, nil
if yDiff >= 0 then
LLx = invAspectRatio * sinCosValue
LLy = sinValue * sinValue
LRy = aspectRatio * sinCosValue
LRx = 1 - LLy
ULx = LLy
ULy = 1 - LRy
URx = 1 - LLx
URy = LRx
else
LLx = sinValue * sinValue
LLy = -aspectRatio * sinCosValue
LRx = 1 + invAspectRatio * sinCosValue
LRy = LLx
ULx = 1 - LRx
ULy = 1 - LLx
URy = 1 - LLy
URx = ULy
end
line:SetTexCoord(ULx, ULy, LLx, LLy, URx, URy, LRx, LRy)
-- calculate and set texture anchors
local xCenter = (xFrom + xTo) / 2
local yCenter = (yFrom + yTo) / 2
local halfWidth = (xDiff + invAspectRatio * abs(yDiff) + thickness) / 2
local halfHeight = (abs(yDiff) + invAspectRatio * xDiff + thickness) / 2
line:SetPoint("BOTTOMLEFT", xCenter - halfWidth, yCenter - halfHeight)
line:SetPoint("TOPRIGHT", line:GetParent(), "BOTTOMLEFT", xCenter + halfWidth, yCenter + halfHeight)
local minY = min(yFrom, yTo)
local maxY = max(yFrom, yTo)
local r, g, b, a = color:GetFractionalRGBA()
local barMaxAlpha = Math.Scale(minY, 0, plotHeight, 0, MAX_FILL_ALPHA * a)
local topMaxAlpha = Math.Scale(maxY, 0, plotHeight, 0, MAX_FILL_ALPHA * a)
line:SetVertexColor(r, g, b, a)
local fillTop = self:_AcquireTexture("ARTWORK", -1)
fillTop:SetTexture("Interface\\AddOns\\TradeSkillMaster\\Media\\triangle")
if yFrom < yTo then
fillTop:SetTexCoord(0, 0, 0, 1, 1, 0, 1, 1)
else
fillTop:SetTexCoord(1, 0, 1, 1, 0, 0, 0, 1)
end
fillTop:SetGradientAlpha("VERTICAL", r, g, b, barMaxAlpha, r, g, b, topMaxAlpha)
fillTop:SetPoint("BOTTOMLEFT", xFrom, minY)
fillTop:SetPoint("TOPRIGHT", fillTop:GetParent(), "BOTTOMLEFT", xTo, maxY)
local fillBar = self:_AcquireTexture("ARTWORK", -1)
fillBar:SetTexture("Interface\\Buttons\\WHITE8X8")
fillBar:SetGradientAlpha("VERTICAL", r, g, b, 0, r, g, b, barMaxAlpha)
fillBar:SetPoint("BOTTOMLEFT", xFrom, 0)
fillBar:SetPoint("TOPRIGHT", fillBar:GetParent(), "BOTTOMLEFT", xTo, minY)
return line
end
function Graph._AcquireLine(self, layer, subLayer)
local line = self:_AcquireTexture(layer, subLayer)
line:SetTexture("Interface\\AddOns\\TradeSkillMaster\\Media\\line.tga")
return line
end
function Graph._AcquireTexture(self, layer, subLayer)
local plot = self:_GetBaseFrame().plot
local result = tremove(self._freeTextures) or plot:CreateTexture()
tinsert(self._usedTextures, result)
result:SetParent(plot)
result:Show()
result:SetDrawLayer(layer, subLayer)
return result
end
function Graph._ReleaseAllTextures(self)
while #self._usedTextures > 0 do
local texture = tremove(self._usedTextures)
texture:SetTexture(nil)
texture:SetVertexColor(0, 0, 0, 0)
texture:SetTexCoord(0, 0, 0, 1, 1, 0, 1, 1)
texture:SetWidth(0)
texture:SetHeight(0)
texture:ClearAllPoints()
texture:Hide()
tinsert(self._freeTextures, texture)
end
end
function Graph._AcquireFontString(self, font)
local plot = self:_GetBaseFrame().plot
local result = tremove(self._freeFontStrings) or plot:CreateFontString()
tinsert(self._usedFontStrings, result)
result:SetParent(plot)
result:Show()
result:SetFont(font:GetWowFont())
result:SetTextColor(Theme.GetColor("TEXT"):GetFractionalRGBA())
return result
end
function Graph._ReleaseAllFontStrings(self)
while #self._usedFontStrings > 0 do
local fontString = tremove(self._usedFontStrings)
fontString:SetWidth(0)
fontString:SetHeight(0)
fontString:ClearAllPoints()
fontString:Hide()
tinsert(self._freeFontStrings, fontString)
end
end
function Graph._GetCursorClosestPoint(self)
local plotFrame = self:_GetBaseFrame().plot
local xPos = GetCursorPosition() / plotFrame:GetEffectiveScale()
local fromMin = plotFrame:GetLeft()
local fromMax = plotFrame:GetRight()
-- Convert the cursor position to be relative to the plotted x values
xPos = Math.Scale(Math.Bound(xPos, fromMin, fromMax), fromMin, fromMax, self._zoomStart, self._zoomEnd)
-- Find the closest point to the cursor (based on the x distance)
local closestX, closestY = nil, nil
for _, x in ipairs(self._xValuesFiltered) do
local y = self._yLookup[x]
local xDist = abs(x - xPos)
if not closestX or xDist < abs(closestX - xPos) then
closestX = x
closestY = y
end
end
assert(closestY)
return closestX, closestY
end
function Graph._XValueToPlotCoord(self, xValue)
local plotFrame = self:_GetBaseFrame().plot
return Math.Scale(xValue, self._zoomStart, self._zoomEnd, 0, plotFrame:GetWidth())
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.PlotFrameOnEnter(self)
self._isMouseOver = true
self:Draw()
local plotFrame = self:_GetBaseFrame().plot
ScriptWrapper.Set(plotFrame, "OnUpdate", private.PlotFrameOnUpdate, self)
end
function private.PlotFrameOnLeave(self)
self._isMouseOver = false
end
function private.PlotFrameOnUpdate(self)
local plotFrame = self:_GetBaseFrame().plot
local closestX, closestY = self:_GetCursorClosestPoint()
local xCoord = self:_XValueToPlotCoord(closestX)
local yCoord = Math.Scale(closestY, self._yMin, self._yMax, 0, plotFrame:GetHeight())
if self._isMouseOver then
plotFrame.dot:Show()
plotFrame.dot:ClearAllPoints()
plotFrame.dot:SetPoint("CENTER", plotFrame, "BOTTOMLEFT", xCoord, yCoord)
plotFrame.hoverLine:Show()
plotFrame.hoverLine:SetColorTexture(Theme.GetColor("INDICATOR_ALT"):GetFractionalRGBA())
plotFrame.hoverLine:ClearAllPoints()
plotFrame.hoverLine:SetPoint("TOP", plotFrame, "TOPLEFT", xCoord, 0)
plotFrame.hoverLine:SetPoint("BOTTOM", plotFrame, "BOTTOMLEFT", xCoord, 0)
plotFrame.hoverText:Show()
plotFrame.hoverText:SetWidth(1000)
plotFrame.hoverText:SetText(self._yFormatFunc(closestY, nil, true))
local textWidth = plotFrame.hoverText:GetStringWidth()
plotFrame.hoverText:SetWidth(textWidth)
plotFrame.hoverText:ClearAllPoints()
if xCoord - textWidth / 2 < 0 then
plotFrame.hoverText:SetPoint("BOTTOMLEFT", plotFrame, "TOPLEFT", 0, PLOT_Y_MARGIN)
elseif textWidth / 2 + xCoord > plotFrame:GetWidth() then
plotFrame.hoverText:SetPoint("BOTTOMRIGHT", plotFrame, "TOPRIGHT", 0, PLOT_Y_MARGIN)
else
plotFrame.hoverText:SetPoint("BOTTOM", plotFrame, "TOPLEFT", xCoord, PLOT_Y_MARGIN)
end
else
plotFrame.dot:Hide()
plotFrame.hoverLine:Hide()
plotFrame.hoverText:Hide()
end
if self._selectionStartX then
local startXCoord = self:_XValueToPlotCoord(self._selectionStartX)
local selectionMinX = min(startXCoord, xCoord)
local selectionMaxX = max(startXCoord, xCoord)
plotFrame.selectionBox:Show()
local r, g, b, a = Theme.GetColor("INDICATOR_ALT"):GetFractionalRGBA()
assert(a == 1)
plotFrame.selectionBox:SetColorTexture(r, g, b, SELECTION_ALPHA)
plotFrame.selectionBox:ClearAllPoints()
plotFrame.selectionBox:SetPoint("TOPLEFT", plotFrame, selectionMinX, 0)
plotFrame.selectionBox:SetPoint("BOTTOMRIGHT", plotFrame, "BOTTOMLEFT", selectionMaxX, 0)
else
plotFrame.selectionBox:Hide()
end
local isHovered = self._isMouseOver or self._selectionStartX
if not isHovered then
self:Draw()
ScriptWrapper.Clear(plotFrame, "OnUpdate")
end
if self._onHoverUpdate then
self:_onHoverUpdate(isHovered and closestX or nil)
end
end
function private.PlotFrameOnMouseDown(self, mouseButton)
if mouseButton ~= "LeftButton" then
return
end
assert(self._isMouseOver)
self._selectionStartX = self:_GetCursorClosestPoint()
end
function private.PlotFrameOnMouseUp(self, mouseButton)
if mouseButton ~= "LeftButton" then
return
end
local currentX = self:_GetCursorClosestPoint()
local startX = min(self._selectionStartX, currentX)
local endX = max(self._selectionStartX, currentX)
self._selectionStartX = nil
local plotFrame = self:_GetBaseFrame().plot
plotFrame.selectionBox:Hide()
if startX ~= endX and (startX ~= self._zoomStart or endX ~= self._zoomEnd) then
self._zoomStart = startX
self._zoomEnd = endX
self:Draw()
if self._onZoomChanged then
self:_onZoomChanged()
end
end
end

View File

@@ -0,0 +1,360 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Group Selector UI Element Class.
-- A group selector is an element which can be used to prompt the user to select a list of groups, usually for
-- filtering. It is a subclass of the @{Element} class.
-- @classmod GroupSelector
local _, TSM = ...
local L = TSM.Include("Locale").GetTable()
local Table = TSM.Include("Util.Table")
local Analytics = TSM.Include("Util.Analytics")
local Theme = TSM.Include("Util.Theme")
local NineSlice = TSM.Include("Util.NineSlice")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local UIElements = TSM.Include("UI.UIElements")
local GroupSelector = TSM.Include("LibTSMClass").DefineClass("GroupSelector", TSM.UI.Element)
UIElements.Register(GroupSelector)
TSM.UI.GroupSelector = GroupSelector
local private = {}
local TEXT_MARGIN = 8
local ICON_MARGIN = 8
local DEFAULT_CONTEXT = { selected = {}, collapsed = {} }
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function GroupSelector.__init(self)
local frame = UIElements.CreateFrame(self, "Button")
ScriptWrapper.Set(frame, "OnClick", private.OnClick, self)
self.__super:__init(frame)
frame.text = UIElements.CreateFontString(self, frame)
frame.text:SetPoint("TOPLEFT", TEXT_MARGIN, 0)
frame.text:SetPoint("BOTTOMRIGHT", -ICON_MARGIN - TSM.UI.TexturePacks.GetWidth("iconPack.18x18/Add/Default") - TEXT_MARGIN, 0)
frame.text:SetJustifyH("LEFT")
frame.text:SetJustifyV("MIDDLE")
frame.icon = frame:CreateTexture(nil, "ARTWORK")
frame.icon:SetPoint("RIGHT", -ICON_MARGIN, 0)
frame.iconBtn = CreateFrame("Button", nil, frame)
frame.iconBtn:SetAllPoints(frame.icon)
ScriptWrapper.Set(frame.iconBtn, "OnClick", private.OnIconClick, self)
self._nineSlice = NineSlice.New(frame)
self._groupTreeContext = CopyTable(DEFAULT_CONTEXT)
self._hintText = ""
self._selectedText = L["%d groups"]
self._singleSelection = nil
self._onSelectionChanged = nil
self._customQueryFunc = nil
self._showCreateNew = false
end
function GroupSelector.Release(self)
wipe(self._groupTreeContext.collapsed)
wipe(self._groupTreeContext.selected)
self._hintText = ""
self._selectedText = L["%d groups"]
self._singleSelection = nil
self._onSelectionChanged = nil
self._customQueryFunc = nil
self._showCreateNew = false
self.__super:Release()
end
--- Sets the hint text.
-- @tparam GroupSelector self The group selector object
-- @tparam string text The hint text
-- @treturn GroupSelector The group selector object
function GroupSelector.SetHintText(self, text)
assert(type(text) == "string")
self._hintText = text
return self
end
--- Sets the selected text.
-- @tparam GroupSelector self The group selector object
-- @tparam string text The selected text (with a %d formatter for the number of groups)
-- @treturn GroupSelector The group selector object
function GroupSelector.SetSelectedText(self, text)
assert(type(text) == "string" and strmatch(text, "%%d"))
self._selectedText = text
return self
end
--- Registers a script handler.
-- @tparam GroupSelector self The group selector object
-- @tparam string script The script to register for (supported scripts: `OnSelectionChanged`)
-- @tparam function handler The script handler which will be called with the group selector object followed by any
-- arguments to the script
-- @treturn GroupSelector The group selector object
function GroupSelector.SetScript(self, script, handler)
if script == "OnSelectionChanged" then
self._onSelectionChanged = handler
else
error("Unknown GroupSelector script: "..tostring(script))
end
return self
end
--- Sets a function to generate a custom query to use for the group tree
-- @tparam GroupSelector self The group selector object
-- @tparam function func A function to call to create the custom query (gets auto-released by the GroupTree)
-- @treturn GroupSelector The group selector object
function GroupSelector.SetCustomQueryFunc(self, func)
self._customQueryFunc = func
return self
end
--- Adds the "Create New Group" option to the group tree
-- @tparam GroupSelector self The group selector object
-- @treturn GroupSelector The group selector object
function GroupSelector.AddCreateNew(self)
self._showCreateNew = true
return self
end
--- Sets the selection to only handle single selection.
-- @tparam GroupSelector self The group selector object
-- @tparam boolean enabled The state of the single selection
-- @treturn GroupSelector The group selector object
function GroupSelector.SetSingleSelection(self, enabled)
self._singleSelection = enabled
return self
end
--- Returns the single selected group path.
-- @tparam GroupSelector self The group selector object
function GroupSelector.GetSelection(self)
assert(self._singleSelection)
return next(self._groupTreeContext.selected)
end
--- Sets the single selected group path.
-- @tparam GroupSelector self The group selector object
-- @tparam string|table selection The selected group(s) or nil if nothing should be selected
-- @treturn GroupSelector The group selector object
function GroupSelector.SetSelection(self, selection)
wipe(self._groupTreeContext.selected)
if not selection then
return self
end
if self._singleSelection then
self._groupTreeContext.selected[selection] = true
else
for groupPath in pairs(selection) do
self._groupTreeContext.selected[groupPath] = true
end
end
return self
end
--- Returns an iterator for all selected groups.
-- @tparam GroupSelector self The group selector object
-- @return An iterator which iterates over the selected groups and has the following values: `groupPath`
function GroupSelector.SelectedGroupIterator(self)
return pairs(self._groupTreeContext.selected)
end
--- Clears all selected groups.
-- @tparam GroupSelector self The group selector object
-- @tparam boolean silent Don't call the selection changed callback
-- @treturn GroupSelector The group selector object
function GroupSelector.ClearSelectedGroups(self, silent)
wipe(self._groupTreeContext.selected)
if not silent and self._onSelectionChanged then
self:_onSelectionChanged()
end
return self
end
function GroupSelector.Draw(self)
self.__super:Draw()
local frame = self:_GetBaseFrame()
frame.text:SetFont(Theme.GetFont("BODY_BODY2"):GetWowFont())
local numGroups = Table.Count(self._groupTreeContext.selected)
frame.text:SetText(numGroups == 0 and self._hintText or (self._singleSelection and TSM.Groups.Path.Format(next(self._groupTreeContext.selected)) or format(self._selectedText, numGroups)))
TSM.UI.TexturePacks.SetTextureAndSize(frame.icon, numGroups == 0 and "iconPack.18x18/Add/Default" or "iconPack.18x18/Close/Default")
self._nineSlice:SetStyle("rounded")
self._nineSlice:SetVertexColor(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function GroupSelector._CreateQuery(self)
local query = nil
if self._customQueryFunc then
query = self._customQueryFunc()
else
query = TSM.Groups.CreateQuery()
end
if self._singleSelection then
query:NotEqual("groupPath", TSM.CONST.ROOT_GROUP_PATH)
end
return query
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.OnClick(self)
self:GetBaseElement():ShowDialogFrame(UIElements.New("Frame", "frame", "DIALOG")
:SetLayout("VERTICAL")
:SetSize(464, 500)
:SetPadding(8)
:AddAnchor("CENTER")
:SetBackgroundColor("FRAME_BG", true)
:SetMouseEnabled(true)
:AddChild(UIElements.New("Frame", "header")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(0, 0, 0, 8)
:AddChild(UIElements.New("Text", "title")
:SetMargin(32, 8, 0, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetJustifyH("CENTER")
:SetText(L["Select Group"])
)
:AddChild(UIElements.New("Button", "closeBtn")
:SetBackgroundAndSize("iconPack.24x24/Close/Default")
:SetScript("OnClick", private.DialogCloseBtnOnClick)
)
)
:AddChild(UIElements.New("Frame", "container")
:SetLayout("VERTICAL")
:SetPadding(2)
:SetBackgroundColor("PRIMARY_BG")
:SetBorderColor("ACTIVE_BG")
:AddChild(UIElements.New("Frame", "header")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(8)
:AddChild(UIElements.New("Input", "input")
:AllowItemInsert(true)
:SetIconTexture("iconPack.18x18/Search")
:SetClearButtonEnabled(true)
:SetHintText(L["Search Groups"])
:SetScript("OnValueChanged", private.DialogFilterOnValueChanged)
)
:AddChild(UIElements.New("Button", "expandAllBtn")
:SetSize(24, 24)
:SetMargin(8, 0, 0, 0)
:SetBackground("iconPack.18x18/Expand All")
:SetScript("OnClick", private.ExpandAllGroupsOnClick)
:SetTooltip(L["Expand / Collapse All Groups"])
)
:AddChildIf(not self._singleSelection, UIElements.New("Button", "selectAllBtn")
:SetSize(24, 24)
:SetMargin(8, 0, 0, 0)
:SetBackground("iconPack.18x18/Select All")
:SetScript("OnClick", private.SelectAllGroupsOnClick)
:SetTooltip(L["Select / Deselect All Groups"])
)
)
:AddChildIf(self._showCreateNew, UIElements.New("Button", "createGroup")
:SetHeight(24)
:SetMargin(8, 8, 0, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetJustifyH("LEFT")
:SetIcon("iconPack.14x14/Add/Circle", "LEFT")
:SetText(L["Create New Group"])
:SetScript("OnClick", private.CreateGroupOnClick)
)
:AddChild(UIElements.New(self._singleSelection and "SelectionGroupTree" or "ApplicationGroupTree", "groupTree")
:SetContext(self)
:SetContextTable(self._groupTreeContext, DEFAULT_CONTEXT)
:SetQuery(self:_CreateQuery())
)
)
:AddChild(UIElements.New("ActionButton", "groupBtn")
:SetHeight(24)
:SetMargin(0, 0, 8, 0)
:SetContext(self)
:SetText(L["Select Group"])
:SetScript("OnClick", private.DialogSelectOnClick)
)
)
end
function private.OnIconClick(self)
if Table.Count(self._groupTreeContext.selected) > 0 then
self:ClearSelectedGroups()
self:Draw()
if self._onSelectionChanged then
self:_onSelectionChanged()
end
else
private.OnClick(self)
end
end
function private.DialogCloseBtnOnClick(button)
local self = button:GetElement("__parent.__parent.groupBtn"):GetContext()
button:GetBaseElement():HideDialog()
self:Draw()
if self._onSelectionChanged then
self:_onSelectionChanged()
end
end
function private.DialogFilterOnValueChanged(input)
input:GetElement("__parent.__parent.groupTree")
:SetSearchString(strlower(input:GetValue()))
:Draw()
end
function private.ExpandAllGroupsOnClick(button)
button:GetElement("__parent.__parent.groupTree")
:ToggleExpandAll()
end
function private.SelectAllGroupsOnClick(button)
button:GetElement("__parent.__parent.groupTree")
:ToggleSelectAll()
end
function private.DialogSelectOnClick(button)
local self = button:GetContext()
button:GetBaseElement():HideDialog()
self:Draw()
if self._onSelectionChanged then
self:_onSelectionChanged()
end
end
function private.CreateGroupOnClick(button)
local newGroupPath = L["New Group"]
if TSM.Groups.Exists(newGroupPath) then
local num = 1
while TSM.Groups.Exists(newGroupPath.." "..num) do
num = num + 1
end
newGroupPath = newGroupPath.." "..num
end
TSM.Groups.Create(newGroupPath)
Analytics.Action("CREATED_GROUP", newGroupPath)
button:GetElement("__parent.groupTree")
:UpdateData()
:SetSelection(newGroupPath)
:Draw()
end

View File

@@ -0,0 +1,345 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- GroupTree UI Element Class.
-- A group tree is an abstract element which displays TSM groups. It is a subclass of the @{ScrollingTable} class.
-- @classmod GroupTree
local _, TSM = ...
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
local String = TSM.Include("Util.String")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Theme = TSM.Include("Util.Theme")
local UIElements = TSM.Include("UI.UIElements")
local GroupTree = TSM.Include("LibTSMClass").DefineClass("GroupTree", TSM.UI.ScrollingTable, "ABSTRACT")
UIElements.Register(GroupTree)
TSM.UI.GroupTree = GroupTree
local private = {}
local EXPANDER_SPACING = 2
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function GroupTree.__init(self)
self.__super:__init()
self:SetRowHeight(24)
self._allData = {}
self._contextTable = nil
self._defaultContextTable = nil
self._hasChildrenLookup = {}
self._query = nil
self._searchStr = ""
self._moduleOperationFilter = nil
end
function GroupTree.Acquire(self)
self._headerHidden = true
self.__super:Acquire()
self:GetScrollingTableInfo()
:NewColumn("group")
:SetFont("BODY_BODY2")
:SetJustifyH("LEFT")
:SetTextFunction(private.GetGroupText)
:SetExpanderStateFunction(private.GetExpanderState)
:SetFlagStateFunction(private.GetFlagState)
:SetTooltipFunction(private.GetTooltip)
:Commit()
:Commit()
end
function GroupTree.Release(self)
wipe(self._allData)
if self._query then
self._query:Release()
self._query = nil
end
self._searchStr = ""
self._moduleOperationFilter = nil
self._contextTable = nil
self._defaultContextTable = nil
wipe(self._hasChildrenLookup)
for _, row in ipairs(self._rows) do
ScriptWrapper.Clear(row._frame, "OnDoubleClick")
end
self.__super:Release()
self:SetRowHeight(24)
end
--- Sets the context table.
-- This table can be used to preserve collapsed state across lifecycles of the group tree and even WoW sessions if it's
-- within the settings DB.
-- @tparam GroupTree self The group tree object
-- @tparam table tbl The context table
-- @tparam table defaultTbl The default table (required fields: `collapsed`)
-- @treturn GroupTree The group tree object
function GroupTree.SetContextTable(self, tbl, defaultTbl)
assert(type(defaultTbl.collapsed) == "table")
tbl.collapsed = tbl.collapsed or CopyTable(defaultTbl.collapsed)
self._contextTable = tbl
self._defaultContextTable = defaultTbl
return self
end
--- Sets the context table from a settings object.
-- @tparam GroupTree self The group tree object
-- @tparam Settings settings The settings object
-- @tparam string key The setting key
-- @treturn GroupTree The group tree object
function GroupTree.SetSettingsContext(self, settings, key)
return self:SetContextTable(settings[key], settings:GetDefaultReadOnly(key))
end
--- Sets the query used to populate the group tree.
-- @tparam GroupTree self The group tree object
-- @tparam DatabaseQuery query The database query object
-- @tparam[opt=nil] string moduleName The name of the module to filter visible groups to only ones with operations
-- @treturn GroupTree The group tree object
function GroupTree.SetQuery(self, query, moduleName)
assert(query)
if self._query then
self._query:Release()
end
self._query = query
self._query:SetUpdateCallback(private.QueryUpdateCallback, self)
self._moduleOperationFilter = moduleName
self:UpdateData()
return self
end
function GroupTree.SetScript(self, script, handler)
-- GroupTree doesn't support any scripts
error("Unknown GroupTree script: "..tostring(script))
return self
end
--- Sets the search string.
-- This search string is used to filter the groups which are displayed in the group tree.
-- @tparam GroupTree self The group tree object
-- @tparam string searchStr The search string which filters the displayed groups
-- @treturn GroupTree The group tree object
function GroupTree.SetSearchString(self, searchStr)
self._searchStr = String.Escape(searchStr)
self:UpdateData()
return self
end
--- Expand every group.
-- @tparam GroupTree self The application group tree object
-- @treturn GroupTree The application group tree object
function GroupTree.ExpandAll(self)
for _, groupPath in ipairs(self._allData) do
if groupPath ~= TSM.CONST.ROOT_GROUP_PATH and self._hasChildrenLookup[groupPath] and self._contextTable.collapsed[groupPath] then
self:_SetCollapsed(groupPath, false)
end
end
self:UpdateData(true)
return self
end
--- Collapse every group.
-- @tparam GroupTree self The application group tree object
-- @treturn GroupTree The application group tree object
function GroupTree.CollapseAll(self)
for _, groupPath in ipairs(self._allData) do
if groupPath ~= TSM.CONST.ROOT_GROUP_PATH and self._hasChildrenLookup[groupPath] and not self._contextTable.collapsed[groupPath] then
self:_SetCollapsed(groupPath, true)
end
end
self:UpdateData(true)
return self
end
--- Toggle the expand/collapse all state of the group tree.
-- @tparam GroupTree self The application group tree object
-- @treturn GroupTree The application group tree object
function GroupTree.ToggleExpandAll(self)
if next(self._contextTable.collapsed) then
-- at least one group is collapsed, so expand everything
self:ExpandAll()
else
-- nothing is collapsed, so collapse everything
self:CollapseAll()
end
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function GroupTree._GetTableRow(self, isHeader)
local row = self.__super:_GetTableRow(isHeader)
if not isHeader then
ScriptWrapper.Set(row._frame, "OnDoubleClick", private.RowOnDoubleClick, row)
end
return row
end
function GroupTree._CanResizeCols(self)
return false
end
function GroupTree._UpdateData(self)
-- update our groups list
wipe(self._hasChildrenLookup)
wipe(self._allData)
wipe(self._data)
local groups = TempTable.Acquire()
if self._moduleOperationFilter then
local shouldKeep = TempTable.Acquire()
for _, row in self._query:Iterator() do
local groupPath = row:GetField("groupPath")
shouldKeep[groupPath] = row:GetField("has"..self._moduleOperationFilter.."Operation")
if shouldKeep[groupPath] then
shouldKeep[TSM.CONST.ROOT_GROUP_PATH] = true
-- add all parent groups to the keep table as well
local checkPath = TSM.Groups.Path.GetParent(groupPath)
while checkPath and checkPath ~= TSM.CONST.ROOT_GROUP_PATH do
shouldKeep[checkPath] = true
checkPath = TSM.Groups.Path.GetParent(checkPath)
end
end
end
for _, row in self._query:Iterator() do
local groupPath = row:GetField("groupPath")
if shouldKeep[groupPath] then
tinsert(groups, groupPath)
end
end
TempTable.Release(shouldKeep)
else
for _, row in self._query:Iterator() do
tinsert(groups, row:GetField("groupPath"))
end
end
-- remove collapsed state for any groups which no longer exist or no longer have children
local pathExists = TempTable.Acquire()
for i, groupPath in ipairs(groups) do
pathExists[groupPath] = true
local nextGroupPath = groups[i + 1]
self._hasChildrenLookup[groupPath] = nextGroupPath and TSM.Groups.Path.IsChild(nextGroupPath, groupPath) or nil
end
for groupPath in pairs(self._contextTable.collapsed) do
if groupPath == TSM.CONST.ROOT_GROUP_PATH or not pathExists[groupPath] or not self._hasChildrenLookup[groupPath] then
self._contextTable.collapsed[groupPath] = nil
end
end
TempTable.Release(pathExists)
for _, groupPath in ipairs(groups) do
tinsert(self._allData, groupPath)
if self._searchStr ~= "" or not self:_IsGroupHidden(groupPath) then
local groupName = groupPath == TSM.CONST.ROOT_GROUP_PATH and L["Base Group"] or TSM.Groups.Path.GetName(groupPath)
if strmatch(strlower(groupName), self._searchStr) and (self._searchStr == "" or groupPath ~= TSM.CONST.ROOT_GROUP_PATH) then
tinsert(self._data, groupPath)
end
end
end
TempTable.Release(groups)
end
function GroupTree._IsGroupHidden(self, data)
if data == TSM.CONST.ROOT_GROUP_PATH then
return false
elseif self._contextTable.collapsed[TSM.CONST.ROOT_GROUP_PATH] then
return true
end
local parent = TSM.Groups.Path.GetParent(data)
while parent and parent ~= TSM.CONST.ROOT_GROUP_PATH do
if self._contextTable.collapsed[parent] then
return true
end
parent = TSM.Groups.Path.GetParent(parent)
end
return false
end
function GroupTree._SetCollapsed(self, data, collapsed)
self._contextTable.collapsed[data] = collapsed or nil
end
function GroupTree._IsSelected(self, data)
return false
end
function GroupTree._HandleRowClick(self, data, mouseButton)
if mouseButton == "RightButton" and self._searchStr == "" and data ~= TSM.CONST.ROOT_GROUP_PATH and self._hasChildrenLookup[data] then
self:_SetCollapsed(data, not self._contextTable.collapsed[data])
self:UpdateData(true)
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetGroupText(self, data)
local groupName = data == TSM.CONST.ROOT_GROUP_PATH and L["Base Group"] or TSM.Groups.Path.GetName(data)
if data ~= TSM.CONST.ROOT_GROUP_PATH then
groupName = Theme.GetGroupColor(select('#', strsplit(TSM.CONST.GROUP_SEP, data))):ColorText(groupName)
end
return groupName
end
function private.GetExpanderState(self, data)
local indentWidth = nil
local searchIsActive = self._searchStr ~= ""
if data == TSM.CONST.ROOT_GROUP_PATH then
indentWidth = -TSM.UI.TexturePacks.GetWidth("iconPack.14x14/Caret/Right") + EXPANDER_SPACING
else
local level = select('#', strsplit(TSM.CONST.GROUP_SEP, data))
indentWidth = (searchIsActive and 0 or (level - 1)) * (TSM.UI.TexturePacks.GetWidth("iconPack.14x14/Caret/Right") + EXPANDER_SPACING)
end
return not searchIsActive and data ~= TSM.CONST.ROOT_GROUP_PATH and self._hasChildrenLookup[data], not self._contextTable.collapsed[data], nil, indentWidth, EXPANDER_SPACING, true
end
function private.GetFlagState(self, data, isMouseOver)
if data == TSM.CONST.ROOT_GROUP_PATH then
return true, Theme.GetColor("TEXT")
end
local level = select('#', strsplit(TSM.CONST.GROUP_SEP, data))
local levelColor = Theme.GetGroupColor(level)
local color = (self:_IsSelected(data) or isMouseOver) and levelColor or Theme.GetColor("PRIMARY_BG_ALT")
return true, color
end
function private.GetTooltip(self, data)
if self._searchStr == "" then
return nil
end
return TSM.Groups.Path.Format(data), true
end
function private.QueryUpdateCallback(_, _, self)
self:UpdateData(true)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.RowOnDoubleClick(row, mouseButton)
if mouseButton ~= "LeftButton" then
return
end
local data = row:GetData()
local self = row._scrollingTable
assert(self._searchStr == "" and data ~= TSM.CONST.ROOT_GROUP_PATH and self._hasChildrenLookup[data])
self:_SetCollapsed(data, not self._contextTable.collapsed[data])
self:UpdateData(true)
end

351
Core/UI/Elements/Input.lua Normal file
View File

@@ -0,0 +1,351 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Input UI Element Class.
-- The input element allows the user to enter text. It is a subclass of the @{BaseInput} class.
-- @classmod Input
local _, TSM = ...
local Theme = TSM.Include("Util.Theme")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local ItemLinked = TSM.Include("Service.ItemLinked")
local UIElements = TSM.Include("UI.UIElements")
local Input = TSM.Include("LibTSMClass").DefineClass("Input", TSM.UI.BaseInput)
UIElements.Register(Input)
TSM.UI.Input = Input
local private = {}
local PADDING_LEFT = 8
local PADDING_RIGHT = 8
local PADDING_TOP_BOTTOM = 4
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Input.__init(self)
local frame = UIElements.CreateFrame(self, "EditBox")
self._editBox = frame
self.__super:__init(frame)
self._hintText = UIElements.CreateFontString(self, frame)
self._hintText:SetFont(Theme.GetFont("BODY_BODY3"):GetWowFont())
self._hintText:SetJustifyH("LEFT")
self._hintText:SetJustifyV("MIDDLE")
self._hintText:SetPoint("TOPLEFT", PADDING_LEFT, 0)
self._hintText:SetPoint("BOTTOMRIGHT", -PADDING_RIGHT, 0)
self._icon = frame:CreateTexture(nil, "ARTWORK")
self._icon:SetPoint("RIGHT", -PADDING_RIGHT / 2, 0)
self._clearBtn = CreateFrame("Button", nil, frame)
self._clearBtn:SetAllPoints(self._icon)
ScriptWrapper.Set(self._clearBtn, "OnClick", private.ClearBtnOnClick, self)
self._subIcon = frame:CreateTexture(nil, "ARTWORK")
self._subIcon:SetPoint("LEFT", PADDING_LEFT / 2, 0)
TSM.UI.TexturePacks.SetTextureAndSize(self._subIcon, "iconPack.14x14/Subtract/Default")
self._subBtn = CreateFrame("Button", nil, frame)
self._subBtn:SetAllPoints(self._subIcon)
ScriptWrapper.Set(self._subBtn, "OnClick", private.SubBtnOnClick, self)
ScriptWrapper.SetPropagate(self._subBtn, "OnEnter")
ScriptWrapper.SetPropagate(self._subBtn, "OnLeave")
self._addIcon = frame:CreateTexture(nil, "ARTWORK")
self._addIcon:SetPoint("RIGHT", -PADDING_RIGHT / 2, 0)
TSM.UI.TexturePacks.SetTextureAndSize(self._addIcon, "iconPack.14x14/Add/Default")
self._addBtn = CreateFrame("Button", nil, frame)
self._addBtn:SetAllPoints(self._addIcon)
ScriptWrapper.Set(self._addBtn, "OnClick", private.AddBtnOnClick, self)
ScriptWrapper.SetPropagate(self._addBtn, "OnEnter")
ScriptWrapper.SetPropagate(self._addBtn, "OnLeave")
ScriptWrapper.Set(frame, "OnEnter", private.OnEnter, self)
ScriptWrapper.Set(frame, "OnLeave", private.OnLeave, self)
local function ItemLinkedCallback(name, link)
if self._allowItemInsert == nil or not self:IsVisible() or not self:HasFocus() then
return
end
if self._allowItemInsert == true then
self._editBox:Insert(link)
else
self._editBox:Insert(name)
end
return true
end
ItemLinked.RegisterCallback(ItemLinkedCallback, -1)
self._clearEnabled = false
self._subAddEnabled = false
self._iconTexture = nil
self._autoComplete = nil
self._allowItemInsert = nil
self._lostFocusOnButton = false
end
function Input.Release(self)
self._clearEnabled = false
self._subAddEnabled = false
self._iconTexture = nil
self._autoComplete = nil
self._allowItemInsert = nil
self._lostFocusOnButton = false
self._hintText:SetText("")
self.__super:Release()
end
--- Sets the horizontal justification of the hint text.
-- @tparam Input self The input object
-- @tparam string justifyH The horizontal justification (either "LEFT", "CENTER" or "RIGHT")
-- @treturn Input The input object
function Input.SetHintJustifyH(self, justifyH)
assert(justifyH == "LEFT" or justifyH == "CENTER" or justifyH == "RIGHT")
self._hintText:SetJustifyH(justifyH)
return self
end
--- Sets the vertical justification of the hint text.
-- @tparam Input self The input object
-- @tparam string justifyV The vertical justification (either "TOP", "MIDDLE" or "BOTTOM")
-- @treturn Input The input object
function Input.SetHintJustifyV(self, justifyV)
assert(justifyV == "TOP" or justifyV == "MIDDLE" or justifyV == "BOTTOM")
self._hintText:SetJustifyV(justifyV)
return self
end
--- Sets the auto complete table.
-- @tparam Input self The input object
-- @tparam table tbl A list of strings to auto-complete to
-- @treturn Input The input object
function Input.SetAutoComplete(self, tbl)
assert(type(tbl) == "table")
self._autoComplete = tbl
return self
end
--- Sets the hint text.
-- The hint text is shown when there's no other text in the input.
-- @tparam Input self The input object
-- @tparam string text The hint text
-- @treturn Input The input object
function Input.SetHintText(self, text)
self._hintText:SetText(text)
return self
end
--- Sets whether or not the clear button is enabled.
-- @tparam Input self The input object
-- @tparam boolean enabled Whether or not the clear button is enabled
-- @treturn Input The input object
function Input.SetClearButtonEnabled(self, enabled)
assert(type(enabled) == "boolean")
assert(not self._subAddEnabled)
self._clearEnabled = enabled
return self
end
--- Sets whether or not the sub/add buttons are enabled.
-- @tparam Input self The input object
-- @tparam boolean enabled Whether or not the sub/add buttons are enabled
-- @treturn Input The input object
function Input.SetSubAddEnabled(self, enabled)
assert(type(enabled) == "boolean")
assert(not self._clearEnabled and not self._iconTexture)
self._subAddEnabled = enabled
return self
end
--- Sets the icon texture.
-- @tparam Input self The input object
-- @tparam[opt=nil] string iconTexture The texture string to use for the icon texture
-- @treturn Input The input object
function Input.SetIconTexture(self, iconTexture)
assert(iconTexture == nil or TSM.UI.TexturePacks.IsValid(iconTexture))
assert(not self._subAddEnabled)
self._iconTexture = iconTexture
return self
end
--- Allows inserting an item into the input by linking it while the input has focus.
-- @tparam Input self The input object
-- @tparam[opt=false] boolean insertLink Insert the link instead of the item name
-- @treturn Input The input object
function Input.AllowItemInsert(self, insertLink)
assert(insertLink == true or insertLink == false or insertLink == nil)
self._allowItemInsert = insertLink or false
return self
end
function Input.Draw(self)
self.__super:Draw()
self:_UpdateIconsForValue(self._value)
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Input._GetHintTextColor(self)
local color = Theme.GetColor(self._disabled and "PRIMARY_BG_ALT" or self._backgroundColor)
if color:IsLight() then
return self:_GetTextColor("+HOVER")
else
return self:_GetTextColor("-HOVER")
end
end
function Input._UpdateIconsForValue(self, value)
local frame = self:_GetBaseFrame()
local leftPadding, rightPadding = PADDING_LEFT, PADDING_RIGHT
-- set the hint text
if value == "" and self._hintText:GetText() ~= "" then
self._hintText:SetFont(Theme.GetFont(self._font):GetWowFont())
self._hintText:SetTextColor(self:_GetHintTextColor():GetFractionalRGBA())
self._hintText:Show()
else
self._hintText:Hide()
end
local showSubAdd = self._subAddEnabled and (frame:IsMouseOver() or frame:HasFocus())
if showSubAdd then
self._subIcon:Show()
self._subBtn:Show()
self._addIcon:Show()
self._addBtn:Show()
else
self._subIcon:Hide()
self._subBtn:Hide()
self._addIcon:Hide()
self._addBtn:Hide()
end
-- set the icon
local iconTexture = nil
if self._clearEnabled and value ~= "" then
self._clearBtn:Show()
iconTexture = TSM.UI.TexturePacks.GetColoredKey("iconPack.18x18/Close/Default", self:_GetTextColor())
else
self._clearBtn:Hide()
iconTexture = not frame:HasFocus() and self._iconTexture and TSM.UI.TexturePacks.GetColoredKey(self._iconTexture, self:_GetTextColor()) or nil
end
if iconTexture then
assert(not showSubAdd)
self._icon:Show()
TSM.UI.TexturePacks.SetTextureAndSize(self._icon, iconTexture)
rightPadding = rightPadding + TSM.UI.TexturePacks.GetWidth(iconTexture)
else
self._icon:Hide()
end
frame:SetTextInsets(leftPadding, rightPadding, PADDING_TOP_BOTTOM, PADDING_TOP_BOTTOM)
-- for some reason the text insets don't take effect right away, so on the next frame, we call GetTextInsets() which seems to fix things
ScriptWrapper.Set(frame, "OnUpdate", private.OnUpdate, self)
end
function Input._OnTextChanged(self, value)
self:_UpdateIconsForValue(value)
end
function Input._ShouldKeepFocus(self)
if not IsMouseButtonDown("LeftButton") then
return false
end
if self._clearBtn:IsVisible() and self._clearBtn:IsMouseOver() then
return true
elseif self._subBtn:IsVisible() and self._subBtn:IsMouseOver() then
return true
elseif self._addBtn:IsVisible() and self._addBtn:IsMouseOver() then
return true
else
return false
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Input._OnChar(self, c)
self.__super:_OnChar(c)
if not self._autoComplete then
return
end
local frame = self:_GetBaseFrame()
local text = frame:GetText()
local match = nil
for _, k in ipairs(self._autoComplete) do
local start, ending = strfind(strlower(k), strlower(text), 1, true)
if start == 1 and ending and ending == #text then
match = k
break
end
end
if match and not IsControlKeyDown() then
local compStart = #text
frame:SetText(match)
self:HighlightText(compStart, #match)
frame:GetScript("OnTextChanged")(frame, true)
end
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnUpdate(self)
local frame = self:_GetBaseFrame()
ScriptWrapper.Clear(frame, "OnUpdate")
frame:GetTextInsets()
end
function private.ClearBtnOnClick(self)
assert(self:_SetValueHelper(""))
self._escValue = ""
self._editBox:SetText(self._value)
self:Draw()
end
function private.SubBtnOnClick(self)
local minVal = self._validateContext and strsplit(":", self._validateContext)
local value = tostring(max(tonumber(self:GetValue()) - (IsShiftKeyDown() and 10 or 1), minVal or -math.huge))
if self:_SetValueHelper(value) then
self._escValue = self._value
self:_GetBaseFrame():SetText(value)
self:_UpdateIconsForValue(value)
end
end
function private.AddBtnOnClick(self)
local _, maxVal = nil, nil
if self._validateContext then
_, maxVal = strsplit(":", self._validateContext)
end
local value = tostring(min(tonumber(self:GetValue()) + (IsShiftKeyDown() and 10 or 1), maxVal or math.huge))
if self:_SetValueHelper(value) then
self._escValue = self._value
self:_GetBaseFrame():SetText(value)
self:_UpdateIconsForValue(value)
end
end
function private.OnEnter(self)
self:_UpdateIconsForValue(self._value)
end
function private.OnLeave(self)
self:_UpdateIconsForValue(self._value)
end

View File

@@ -0,0 +1,287 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- ItemList UI Element Class.
-- This element is used for the item lists in the group UI. It is a subclass of the @{ScrollingTable} class.
-- @classmod ItemList
local _, TSM = ...
local ItemString = TSM.Include("Util.ItemString")
local Theme = TSM.Include("Util.Theme")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local ItemInfo = TSM.Include("Service.ItemInfo")
local ItemList = TSM.Include("LibTSMClass").DefineClass("ItemList", TSM.UI.ScrollingTable)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(ItemList)
TSM.UI.ItemList = ItemList
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ItemList.__init(self)
self.__super:__init()
self._rightClickToggle = true
self._allData = {}
self._selectedItems = {}
self._category = {}
self._categoryCollapsed = {}
self._filterFunc = nil
self._onSelectionChangedHandler = nil
end
function ItemList.Acquire(self)
self._headerHidden = true
self.__super:Acquire()
self:SetSelectionDisabled(true)
self:GetScrollingTableInfo()
:NewColumn("item")
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetIconSize(12)
:SetExpanderStateFunction(private.GetExpanderState)
:SetCheckStateFunction(private.GetCheckState)
:SetIconFunction(private.GetItemIcon)
:SetTextFunction(private.GetItemText)
:SetTooltipFunction(private.GetItemTooltip)
:Commit()
:Commit()
end
function ItemList.Release(self)
wipe(self._allData)
wipe(self._selectedItems)
wipe(self._category)
wipe(self._categoryCollapsed)
self._filterFunc = nil
self._onSelectionChangedHandler = nil
for _, row in ipairs(self._rows) do
ScriptWrapper.Clear(row._frame, "OnDoubleClick")
end
self.__super:Release()
end
--- Sets the items.
-- @tparam ItemList self The item list object
-- @tparam table items Either a list of items or list of tables with a `header` field and sub-list of items
-- @tparam boolean redraw Whether or not to redraw the item list
-- @treturn ItemList The item list object
function ItemList.SetItems(self, items, redraw)
wipe(self._allData)
wipe(self._category)
wipe(self._categoryCollapsed)
for _, item in ipairs(items) do
if type(item) == "table" and next(item) then
assert(item.header)
tinsert(self._allData, item.header)
for _, subItem in ipairs(item) do
tinsert(self._allData, subItem)
self._category[subItem] = item.header
end
elseif type(item) ~= "table" then
tinsert(self._allData, item)
self._category[item] = ""
end
end
self:_UpdateData()
wipe(self._selectedItems)
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
if redraw then
-- scroll up to the top
self._vScrollbar:SetValue(0)
self:Draw()
end
return self
end
--- Sets a filter function.
-- @tparam ItemList self The item list object
-- @tparam function func A function which is passed an item and returns true if it should be filtered (not shown)
-- @treturn ItemList The item list object
function ItemList.SetFilterFunction(self, func)
self._filterFunc = func
self:_UpdateData()
return self
end
--- Gets whether or not an item is selected.
-- @tparam ItemList self The item list object
-- @tparam string item The item
-- @treturn boolean Whether or not the item is selected
function ItemList.IsItemSelected(self, item)
return tContains(self._data, item) and self._selectedItems[item]
end
--- Selects all items.
-- @tparam ItemList self The item list object
function ItemList.SelectAll(self)
for _, item in ipairs(self._data) do
if self._category[item] then
self._selectedItems[item] = true
end
end
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
self:Draw()
end
--- Deselects all items.
-- @tparam ItemList self The item list object
function ItemList.ClearSelection(self)
wipe(self._selectedItems)
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
self:Draw()
end
--- Toggle the selection state of the item list.
-- @tparam ItemList self The item list object
-- @treturn ItemList The item list object
function ItemList.ToggleSelectAll(self)
if self:GetNumSelected() == 0 then
self:SelectAll()
else
self:ClearSelection()
end
return self
end
--- Registers a script handler.
-- @tparam ItemList self The item list object
-- @tparam string script The script to register for (supported scripts: `OnSelectionChanged`)
-- @tparam function handler The script handler which will be called with the item list object followed by any arguments
-- to the script
-- @treturn ItemList The item list object
function ItemList.SetScript(self, script, handler)
if script == "OnSelectionChanged" then
self._onSelectionChangedHandler = handler
else
error("Unknown ItemList script: "..tostring(script))
end
return self
end
--- Gets the number of selected items.
-- @tparam ItemList self The item list object
-- @treturn number The number of selected items
function ItemList.GetNumSelected(self)
local num = 0
for _, item in ipairs(self._data) do
if self._selectedItems[item] then
num = num + 1
end
end
return num
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function ItemList._UpdateData(self)
wipe(self._data)
for _, data in ipairs(self._allData) do
if not self:_IsDataHidden(data) then
tinsert(self._data, data)
end
end
end
function ItemList._IsDataHidden(self, data)
if not self._category[data] then
return false
end
if self._categoryCollapsed[self._category[data]] then
return true
end
if self._filterFunc then
return self._filterFunc(data)
end
return false
end
function ItemList._GetTableRow(self, isHeader)
local row = self.__super:_GetTableRow(isHeader)
if not isHeader then
ScriptWrapper.Set(row._frame, "OnDoubleClick", private.RowOnDoubleClick, row)
end
return row
end
function ItemList._HandleRowClick(self, data)
if self._category[data] then
self._selectedItems[data] = not self._selectedItems[data]
else
if IsMouseButtonDown("RightButton") then
return
end
self._categoryCollapsed[data] = not self._categoryCollapsed[data]
self:_UpdateData()
end
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
self:Draw()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.GetExpanderState(self, data)
local isHeading = not self._category[data]
return isHeading, not self._categoryCollapsed[data], isHeading and 0 or 1
end
function private.GetCheckState(self, data)
return self._category[data] and self._selectedItems[data]
end
function private.GetItemIcon(self, data)
if not self._category[data] then
return
end
return ItemInfo.GetTexture(data)
end
function private.GetItemText(self, data)
if self._category[data] then
return TSM.UI.GetColoredItemName(data) or Theme.GetFeedbackColor("RED"):ColorText("?")
else
return data
end
end
function private.GetItemTooltip(self, data)
if not self._category[data] then
return nil
end
return ItemString.Get(data)
end
function private.RowOnDoubleClick(row, mouseButton)
if mouseButton ~= "LeftButton" then
return
end
local self = row._scrollingTable
self:_HandleRowClick(row:GetData())
end

View File

@@ -0,0 +1,204 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- LargeApplicationFrame UI Element Class.
-- This is the base frame of the large TSM windows which have tabs along the top (i.e. MainUI, AuctionUI, CraftingUI).
-- It is a subclass of the @{ApplicationFrame} class.
-- @classmod LargeApplicationFrame
local _, TSM = ...
local LargeApplicationFrame = TSM.Include("LibTSMClass").DefineClass("LargeApplicationFrame", TSM.UI.ApplicationFrame)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(LargeApplicationFrame)
TSM.UI.LargeApplicationFrame = LargeApplicationFrame
local private = {}
local NAV_BAR_SPACING = 16
local NAV_BAR_HEIGHT = 24
local NAV_BAR_RELATIVE_LEVEL = 21
local NAV_BAR_TOP_OFFSET = -8
-- ============================================================================
-- Meta Class Methods
-- ============================================================================
function LargeApplicationFrame.__init(self)
self.__super:__init()
self._buttons = {}
self._selectedButton = nil
self._buttonIndex = {}
end
function LargeApplicationFrame.Acquire(self)
self:SetContentFrame(UIElements.New("Frame", "content")
:SetLayout("VERTICAL")
:SetBackgroundColor("FRAME_BG")
)
self.__super:Acquire()
end
function LargeApplicationFrame.Release(self)
wipe(self._buttons)
wipe(self._buttonIndex)
self._selectedButton = nil
self.__super:Release()
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
--- Sets the context table.
-- This table can be used to preserve position, size, and current page information across lifecycles of the frame and
-- even WoW sessions if it's within the settings DB.
-- @see ApplicationFrame.SetContextTable
-- @tparam LargeApplicationFrame self The large application frame object
-- @tparam table tbl The context table
-- @tparam table defaultTbl Default values (see @{ApplicationFrame.SetContextTable} for fields)
-- @treturn LargeApplicationFrame The large application frame object
function LargeApplicationFrame.SetContextTable(self, tbl, defaultTbl)
assert(defaultTbl.page)
tbl.page = tbl.page or defaultTbl.page
self.__super:SetContextTable(tbl, defaultTbl)
return self
end
--- Adds a top-level navigation button.
-- @tparam LargeApplicationFrame self The large application frame object
-- @tparam string text The button text
-- @tparam function drawCallback The function called when the button is clicked to get the corresponding content
-- @treturn LargeApplicationFrame The large application frame object
function LargeApplicationFrame.AddNavButton(self, text, drawCallback)
local button = UIElements.New("AlphaAnimatedFrame", "NavBar_"..text)
:SetRange(1, 0.3)
:SetDuration(1)
:SetLayout("HORIZONTAL")
:SetRelativeLevel(NAV_BAR_RELATIVE_LEVEL)
:SetContext(drawCallback)
:AddChild(UIElements.New("Button", "button")
:SetText(text)
:SetScript("OnEnter", private.OnNavBarButtonEnter)
:SetScript("OnLeave", private.OnNavBarButtonLeave)
:SetScript("OnClick", private.OnNavBarButtonClicked)
)
self:AddChildNoLayout(button)
tinsert(self._buttons, button)
self._buttonIndex[text] = #self._buttons
if self._buttonIndex[text] == self._contextTable.page then
self:SetSelectedNavButton(text)
end
return self
end
--- Set the selected nav button.
-- @tparam LargeApplicationFrame self The large application frame object
-- @tparam string buttonText The button text
-- @tparam boolean redraw Whether or not to redraw the frame
function LargeApplicationFrame.SetSelectedNavButton(self, buttonText, redraw)
if buttonText == self._selectedButton then
return
end
local index = self._buttonIndex[buttonText]
self._contextTable.page = index
self._selectedButton = buttonText
self._contentFrame:ReleaseAllChildren()
self._contentFrame:AddChild(self._buttons[index]:GetContext()(self))
if redraw then
self:Draw()
end
return self
end
--- Get the selected nav button.
-- @tparam LargeApplicationFrame self The large application frame object
-- @treturn string The text of the selected button
function LargeApplicationFrame.GetSelectedNavButton(self)
return self._selectedButton
end
--- Sets which nav button is pulsing.
-- @tparam LargeApplicationFrame self The large application frame object
-- @tparam ?string buttonText The button text or nil if no nav button should be pulsing
function LargeApplicationFrame.SetPulsingNavButton(self, buttonText)
local index = buttonText and self._buttonIndex[buttonText]
for i, button in ipairs(self._buttons) do
if not index or i ~= index then
button:SetPlaying(false)
elseif not button:IsPlaying() then
button:SetPlaying(true)
end
end
end
function LargeApplicationFrame.Draw(self)
self.__super:Draw()
for i, buttonFrame in ipairs(self._buttons) do
local button = buttonFrame:GetElement("button")
button:SetFont("BODY_BODY1_BOLD")
button:SetTextColor(i == self._contextTable.page and "INDICATOR" or "TEXT_ALT")
button:Draw()
buttonFrame:SetSize(button:GetStringWidth(), NAV_BAR_HEIGHT)
end
local offsetX = 104
for _, buttonFrame in ipairs(self._buttons) do
local buttonWidth = buttonFrame:GetElement("button"):GetStringWidth()
buttonFrame:SetSize(buttonWidth, NAV_BAR_HEIGHT)
buttonFrame:WipeAnchors()
buttonFrame:AddAnchor("TOPLEFT", offsetX, NAV_BAR_TOP_OFFSET)
offsetX = offsetX + buttonWidth + NAV_BAR_SPACING
-- draw the buttons again now that we know their dimensions
buttonFrame:Draw()
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function LargeApplicationFrame._SetResizing(self, resizing)
for _, button in ipairs(self._buttons) do
if resizing then
button:Hide()
else
button:Show()
end
end
self.__super:_SetResizing(resizing)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnNavBarButtonEnter(button)
if button:GetBaseElement():GetSelectedNavButton() == button:GetText() then
return
end
button:SetTextColor("TEXT")
:Draw()
end
function private.OnNavBarButtonLeave(button)
if button:GetBaseElement():GetSelectedNavButton() == button:GetText() then
return
end
button:SetTextColor("TEXT_ALT")
:Draw()
end
function private.OnNavBarButtonClicked(button)
local self = button:GetParentElement():GetParentElement()
self:SetSelectedNavButton(button:GetText(), true)
end

View File

@@ -0,0 +1,301 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- ManagementGroupTree UI Element Class.
-- The management group tree allows for moving, adding, and deleting groups. It also only allows for a single group to
-- be selected. It is a subclass of the @{GroupTree} class.
-- @classmod ManagementGroupTree
local _, TSM = ...
local L = TSM.Include("Locale").GetTable()
local Analytics = TSM.Include("Util.Analytics")
local String = TSM.Include("Util.String")
local Theme = TSM.Include("Util.Theme")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local ManagementGroupTree = TSM.Include("LibTSMClass").DefineClass("ManagementGroupTree", TSM.UI.GroupTree)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(ManagementGroupTree)
TSM.UI.ManagementGroupTree = ManagementGroupTree
local private = {}
local DRAG_SCROLL_SPEED_FACTOR = 12
local MOVE_FRAME_PADDING = 8
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ManagementGroupTree.__init(self)
self.__super:__init()
self._moveFrame = nil
self._selectedGroup = nil
self._onGroupSelectedHandler = nil
self._onNewGroupHandler = nil
self._scrollAmount = 0
end
function ManagementGroupTree.Acquire(self)
self._moveFrame = UIElements.New("Frame", self._id.."_MoveFrame")
:SetLayout("VERTICAL")
:SetHeight(20)
:SetStrata("TOOLTIP")
:SetBackgroundColor("PRIMARY_BG_ALT", true)
:SetBorderColor("INDICATOR")
:SetContext(self)
:AddChild(UIElements.New("Text", "text")
:SetFont("BODY_BODY3")
:SetJustifyH("CENTER")
)
self._moveFrame:SetParent(self:_GetBaseFrame())
self._moveFrame:Hide()
self._moveFrame:SetScript("OnShow", private.MoveFrameOnShow)
self._moveFrame:SetScript("OnUpdate", private.MoveFrameOnUpdate)
self.__super:Acquire()
self:GetScrollingTableInfo()
:GetColById("group")
:SetActionIconInfo(2, 14, private.GetActionIcon, true)
:SetActionIconClickHandler(private.OnActionIconClick)
:Commit()
:Commit()
end
function ManagementGroupTree.Release(self)
self._selectedGroup = nil
self._onGroupSelectedHandler = nil
self._onNewGroupHandler = nil
self._moveFrame:Release()
self._moveFrame = nil
for _, row in ipairs(self._rows) do
row._frame:RegisterForDrag()
ScriptWrapper.Clear(row._frame, "OnDragStart")
ScriptWrapper.Clear(row._frame, "OnDragStop")
for _, button in pairs(row._buttons) do
button:RegisterForDrag()
ScriptWrapper.Clear(button, "OnDragStart")
ScriptWrapper.Clear(button, "OnDragStop")
end
end
self.__super:Release()
end
--- Sets the selected group.
-- @tparam ManagementGroupTree self The management group tree object
-- @tparam string groupPath The selected group's path
-- @tparam boolean redraw Whether or not to redraw the management group tree
-- @treturn ManagementGroupTree The management group tree object
function ManagementGroupTree.SetSelectedGroup(self, groupPath, redraw)
self._selectedGroup = groupPath
if self._onGroupSelectedHandler then
self:_onGroupSelectedHandler(groupPath)
end
if redraw then
-- make sure this group is visible (its parent is expanded)
local parent = TSM.Groups.Path.GetParent(groupPath)
self._contextTable.collapsed[TSM.CONST.ROOT_GROUP_PATH] = nil
while parent and parent ~= TSM.CONST.ROOT_GROUP_PATH do
self._contextTable.collapsed[parent] = nil
parent = TSM.Groups.Path.GetParent(parent)
end
self:UpdateData(true)
self:_ScrollToData(self._selectedGroup)
end
return self
end
--- Registers a script handler.
-- @tparam ManagementGroupTree self The management group tree object
-- @tparam string script The script to register for (supported scripts: `OnGroupSelected`)
-- @tparam function handler The script handler which will be called with the management group tree object followed by
-- any arguments to the script
-- @treturn ManagementGroupTree The management group tree object
function ManagementGroupTree.SetScript(self, script, handler)
if script == "OnGroupSelected" then
self._onGroupSelectedHandler = handler
elseif script == "OnNewGroup" then
self._onNewGroupHandler = handler
else
error("Unknown ManagementGroupTree script: "..tostring(script))
end
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function ManagementGroupTree._GetTableRow(self, isHeader)
local row = self.__super:_GetTableRow(isHeader)
if not isHeader then
row._frame:RegisterForDrag("LeftButton")
ScriptWrapper.Set(row._frame, "OnDragStart", private.RowOnDragStart, row)
ScriptWrapper.Set(row._frame, "OnDragStop", private.RowOnDragStop, row)
for _, button in pairs(row._buttons) do
button:RegisterForDrag("LeftButton")
ScriptWrapper.Set(button, "OnDragStart", private.RowOnDragStart, row)
ScriptWrapper.Set(button, "OnDragStop", private.RowOnDragStop, row)
end
end
return row
end
function ManagementGroupTree._SetCollapsed(self, data, collapsed)
self.__super:_SetCollapsed(data, collapsed)
if collapsed and self._selectedGroup ~= data and strmatch(self._selectedGroup, "^"..String.Escape(data)) then
-- we collapsed a parent of the selected group, so select the group we just collapsed instead
self:SetSelectedGroup(data, true)
end
end
function ManagementGroupTree._IsSelected(self, data)
return data == self._selectedGroup
end
function ManagementGroupTree._HandleRowClick(self, data, mouseButton)
if mouseButton == "RightButton" then
self.__super:_HandleRowClick(data, mouseButton)
return
end
self:SetSelectedGroup(data, true)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetActionIcon(self, data, iconIndex, isMouseOver)
if iconIndex == 1 then
local texturePack = "iconPack.14x14/Add/Circle"
return true, isMouseOver and TSM.UI.TexturePacks.GetColoredKey(texturePack, Theme.GetColor("INDICATOR")) or texturePack
elseif iconIndex == 2 then
if data ~= TSM.CONST.ROOT_GROUP_PATH then
local texturePack = "iconPack.14x14/Delete"
return true, isMouseOver and TSM.UI.TexturePacks.GetColoredKey(texturePack, Theme.GetColor("INDICATOR")) or texturePack
else
return false, nil
end
else
error("Invalid index: "..tostring(iconIndex))
end
end
function private.OnActionIconClick(self, data, iconIndex)
if iconIndex == 1 then
local newGroupPath = TSM.Groups.Path.Join(data, L["New Group"])
if TSM.Groups.Exists(newGroupPath) then
local num = 1
while TSM.Groups.Exists(newGroupPath.." "..num) do
num = num + 1
end
newGroupPath = newGroupPath.." "..num
end
TSM.Groups.Create(newGroupPath)
Analytics.Action("CREATED_GROUP", newGroupPath)
self:SetSelectedGroup(newGroupPath, true)
if self._onNewGroupHandler then
self:_onNewGroupHandler()
end
elseif iconIndex == 2 then
local groupColor = Theme.GetGroupColor(select('#', strsplit(TSM.CONST.GROUP_SEP, data)))
self:GetBaseElement():ShowConfirmationDialog(L["Delete Group?"], format(L["Deleting this group (%s) will also remove any sub-groups attached to this group."], groupColor:ColorText(TSM.Groups.Path.GetName(data))), private.DeleteConfirmed, self, data)
else
error("Invalid index: "..tostring(iconIndex))
end
end
function private.DeleteConfirmed(self, data)
TSM.Groups.Delete(data)
Analytics.Action("DELETED_GROUP", data)
self:SetSelectedGroup(TSM.CONST.ROOT_GROUP_PATH, true)
end
function private.MoveFrameOnShow(frame)
local self = frame:GetContext()
self._scrollAmount = 0
end
function private.MoveFrameOnUpdate(frame)
local self = frame:GetContext()
local uiScale = UIParent:GetEffectiveScale()
local x, y = GetCursorPosition()
x = x / uiScale
y = y / uiScale
frame:_GetBaseFrame():SetPoint("CENTER", UIParent, "BOTTOMLEFT", x, y)
-- figure out if we're above or below the frame for scrolling while dragging
local top = self:_GetBaseFrame():GetTop()
local bottom = self:_GetBaseFrame():GetBottom()
if y > top then
self._scrollAmount = top - y
elseif y < bottom then
self._scrollAmount = bottom - y
else
self._scrollAmount = 0
end
self._vScrollbar:SetValue(self._vScrollbar:GetValue() + self._scrollAmount / DRAG_SCROLL_SPEED_FACTOR)
end
function private.RowOnDragStart(row)
local self = row._scrollingTable
local groupPath = row:GetData()
if groupPath == TSM.CONST.ROOT_GROUP_PATH then
-- don't do anything for the root group
return
end
local level = select('#', strsplit(TSM.CONST.GROUP_SEP, groupPath))
local levelColor = Theme.GetGroupColor(level)
self._dragGroupPath = groupPath
self._moveFrame:Show()
self._moveFrame:SetHeight(self._rowHeight)
local moveFrameText = self._moveFrame:GetElement("text")
moveFrameText:SetTextColor(levelColor)
moveFrameText:SetText(TSM.Groups.Path.GetName(groupPath))
moveFrameText:SetWidth(1000)
moveFrameText:Draw()
self._moveFrame:SetWidth(moveFrameText:GetStringWidth() + MOVE_FRAME_PADDING * 2)
self._moveFrame:Draw()
end
function private.RowOnDragStop(row)
local self = row._scrollingTable
local groupPath = row:GetData()
if groupPath == TSM.CONST.ROOT_GROUP_PATH then
-- don't do anything for the root group
return
end
self._moveFrame:Hide()
local destPath = nil
for _, targetRow in ipairs(self._rows) do
if targetRow:IsMouseOver() then
destPath = targetRow:GetData()
break
end
end
local oldPath = self._dragGroupPath
self._dragGroupPath = nil
if not destPath or destPath == oldPath or TSM.Groups.Path.IsChild(destPath, oldPath) then
return
end
local newPath = TSM.Groups.Path.Join(destPath, TSM.Groups.Path.GetName(oldPath))
if oldPath == newPath then
return
elseif TSM.Groups.Exists(newPath) then
return
end
TSM.Groups.Move(oldPath, newPath)
Analytics.Action("MOVED_GROUP", oldPath, newPath)
self:SetSelectedGroup(newPath, true)
end

View File

@@ -0,0 +1,170 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- MultiLineInput UI Element Class.
-- The input element allows the user to enter text. It is a subclass of the @{BaseInput} class.
-- @classmod MultiLineInput
local _, TSM = ...
local Theme = TSM.Include("Util.Theme")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local MultiLineInput = TSM.Include("LibTSMClass").DefineClass("MultiLineInput", TSM.UI.BaseInput)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(MultiLineInput)
TSM.UI.MultiLineInput = MultiLineInput
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function MultiLineInput.__init(self)
local frame = UIElements.CreateFrame(self, "ScrollFrame")
self._editBox = CreateFrame("EditBox", nil, frame)
self.__super:__init(frame)
frame:EnableMouseWheel(true)
frame:SetClipsChildren(true)
ScriptWrapper.Set(frame, "OnUpdate", private.FrameOnUpdate, self)
ScriptWrapper.Set(frame, "OnMouseWheel", private.FrameOnMouseWheel, self)
ScriptWrapper.Set(frame, "OnMouseUp", private.FrameOnMouseUp, self)
self._scrollbar = TSM.UI.Scrollbar.Create(frame)
ScriptWrapper.Set(self._scrollbar, "OnValueChanged", private.OnScrollbarValueChanged, self)
self._editBox:SetSpacing(4)
self._editBox:SetMultiLine(true)
self._editBox:SetTextInsets(8, 8, 4, 4)
frame:SetScrollChild(self._editBox)
ScriptWrapper.Set(self._editBox, "OnCursorChanged", private.OnCursorChanged, self)
ScriptWrapper.Set(self._editBox, "OnSizeChanged", private.OnSizeChanged, self)
self._scrollValue = 0
self._ignoreEnter = false
end
function MultiLineInput.Acquire(self)
self:SetBackgroundColor("ACTIVE_BG")
self:SetJustifyH("LEFT")
self:SetJustifyV("TOP")
self.__super:Acquire()
self._scrollValue = 0
self._ignoreEnter = false
self._scrollbar:SetValue(0)
end
function MultiLineInput.Draw(self)
self._editBox:SetWidth(self:_GetBaseFrame():GetWidth())
self.__super:Draw()
local maxScroll = self:_GetMaxScroll()
self._scrollbar:SetMinMaxValues(0, maxScroll)
self._scrollbar:SetValue(min(self._scrollValue, maxScroll))
self._scrollbar.thumb:SetHeight(TSM.UI.Scrollbar.GetLength(self._editBox:GetHeight(), self:_GetDimension("HEIGHT")))
end
--- Sets to ignore enter pressed scripts for the input multi-line input.
-- @tparam MultiLineInput self The multi-line input object
-- @treturn MultiLineInput The multi-line input object
function MultiLineInput.SetIgnoreEnter(self)
ScriptWrapper.Clear(self._editBox, "OnEnterPressed")
self._ignoreEnter = true
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function MultiLineInput._OnScrollValueChanged(self, value)
self:_GetBaseFrame():SetVerticalScroll(value)
self._scrollValue = value
end
function MultiLineInput._GetMaxScroll(self)
return max(self._editBox:GetHeight() - self:_GetDimension("HEIGHT"), 0)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnCursorChanged(self, _, y, _, lineHeight)
y = abs(y)
local offset = self._scrollValue - y
if offset > 0 or offset < self:_GetDimension("HEIGHT") + lineHeight then
self._scrollbar:SetValue(y)
end
end
function private.OnSizeChanged(self, _, height)
local maxScroll = self:_GetMaxScroll()
self._scrollbar:SetMinMaxValues(0, maxScroll)
self._scrollbar:SetValue(min(self._scrollValue, maxScroll))
self._scrollbar.thumb:SetHeight(TSM.UI.Scrollbar.GetLength(self._editBox:GetHeight(), self:_GetDimension("HEIGHT")))
end
function private.OnScrollbarValueChanged(self, value)
value = max(min(value, self:_GetMaxScroll()), 0)
self:_OnScrollValueChanged(value)
end
function private.FrameOnUpdate(self)
if (self:_GetBaseFrame():IsMouseOver() and self:_GetMaxScroll() > 0) or self._scrollbar.dragging then
self._scrollbar:Show()
else
self._scrollbar:Hide()
end
end
function private.FrameOnMouseWheel(self, direction)
local parentScroll = nil
local parent = self:GetParentElement()
while parent do
if parent:__isa(TSM.UI.ScrollFrame) then
parentScroll = parent
break
else
parent = parent:GetParentElement()
end
end
if parentScroll then
local minValue, maxValue = self._scrollbar:GetMinMaxValues()
if direction > 0 then
if self._scrollbar:GetValue() == minValue then
local scrollAmount = min(parentScroll:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
parentScroll._scrollbar:SetValue(parentScroll._scrollbar:GetValue() + -1 * direction * scrollAmount)
else
local scrollAmount = min(self:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
self._scrollbar:SetValue(self._scrollbar:GetValue() + -1 * direction * scrollAmount)
end
else
if self._scrollbar:GetValue() == maxValue then
local scrollAmount = min(parentScroll:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
parentScroll._scrollbar:SetValue(parentScroll._scrollbar:GetValue() + -1 * direction * scrollAmount)
else
local scrollAmount = min(self:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
self._scrollbar:SetValue(self._scrollbar:GetValue() + -1 * direction * scrollAmount)
end
end
else
local scrollAmount = min(self:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
self._scrollbar:SetValue(self._scrollbar:GetValue() + -1 * direction * scrollAmount)
end
end
function private.FrameOnMouseUp(self)
self:SetFocused(true)
end

View File

@@ -0,0 +1,292 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Multiselection Dropdown UI Element Class.
-- A dropdown element allows the user to select from a dialog list. It is a subclass of the @{BaseDropdown} class.
-- @classmod MultiselectionDropdown
local _, TSM = ...
local L = TSM.Include("Locale").GetTable()
local Table = TSM.Include("Util.Table")
local Theme = TSM.Include("Util.Theme")
local MultiselectionDropdown = TSM.Include("LibTSMClass").DefineClass("MultiselectionDropdown", TSM.UI.BaseDropdown)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(MultiselectionDropdown)
TSM.UI.MultiselectionDropdown = MultiselectionDropdown
local private = {}
-- ============================================================================
-- Meta Class Methods
-- ============================================================================
function MultiselectionDropdown.__init(self)
self.__super:__init()
self._itemIsSelected = {}
self._settingTableDirect = nil
self._text = self:_GetBaseFrame():CreateFontString()
self._text:SetFont(Theme.GetFont("BODY_BODY3"):GetWowFont())
self._text:Hide()
self._noneSelectionText = L["None Selected"]
self._partialSelectionText = L["%d Selected"]
self._allSelectionText = L["All Selected"]
end
function MultiselectionDropdown.Release(self)
wipe(self._itemIsSelected)
self._settingTableDirect = nil
self._noneSelectionText = L["None Selected"]
self._partialSelectionText = L["%d Selected"]
self._allSelectionText = L["All Selected"]
self.__super:Release()
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
--- Set whether the item is selected.
-- @tparam MultiselectionDropdown self The dropdown object
-- @tparam string item The item
-- @tparam boolean selected Whether or not the item should be selected
-- @treturn MultiselectionDropdown The dropdown object
function MultiselectionDropdown.SetItemSelected(self, item, selected)
self:_SetItemSelectedHelper(item, selected)
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
return self
end
--- Set whether the item is selected by key.
-- @tparam MultiselectionDropdown self The dropdown object
-- @tparam string itemKey The key for the item
-- @tparam boolean selected Whether or not the item should be selected
-- @treturn MultiselectionDropdown The dropdown object
function MultiselectionDropdown.SetItemSelectedByKey(self, itemKey, selected)
self:SetItemSelected(Table.GetDistinctKey(self._itemKeyLookup, itemKey), selected)
return self
end
--- Set the selected items.
-- @tparam MultiselectionDropdown self The dropdown object
-- @tparam table selected A table where the keys are the items to be selected
-- @treturn MultiselectionDropdown The dropdown object
function MultiselectionDropdown.SetSelectedItems(self, selected)
wipe(self._itemIsSelected)
if self._settingTableDirect then
wipe(self._settingTableDirect)
end
for _, item in ipairs(self._items) do
self:_SetItemSelectedHelper(item, selected[item])
end
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
return self
end
--- Set the selected items.
-- @tparam MultiselectionDropdown self The dropdown object
-- @tparam table selected A table where the keys are the items to be selected
-- @treturn MultiselectionDropdown The dropdown object
function MultiselectionDropdown.SetSelectedItemKeys(self, selected)
wipe(self._itemIsSelected)
if self._settingTableDirect then
wipe(self._settingTableDirect)
end
for _, item in ipairs(self._items) do
self:_SetItemSelectedHelper(item, selected[self._itemKeyLookup[item]])
end
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
return self
end
--- Set the unselected items.
-- @tparam MultiselectionDropdown self The dropdown object
-- @tparam table unselected A table where the keys are the items which aren't selected
-- @treturn MultiselectionDropdown The dropdown object
function MultiselectionDropdown.SetUnselectedItemKeys(self, unselected)
wipe(self._itemIsSelected)
if self._settingTableDirect then
wipe(self._settingTableDirect)
end
for _, item in ipairs(self._items) do
self:_SetItemSelectedHelper(item, not unselected[self._itemKeyLookup[item]])
end
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
return self
end
--- Get the currently selected item.
-- @tparam MultiselectionDropdown self The dropdown object
-- @tparam string item The item
-- @treturn ?string The selected item
function MultiselectionDropdown.ItemIsSelected(self, item)
return self._itemIsSelected[item]
end
--- Get the currently selected item.
-- @tparam MultiselectionDropdown self The dropdown object
-- @tparam string|number itemKey The key for the item
-- @treturn boolean Whether or not the item is selected
function MultiselectionDropdown.ItemIsSelectedByKey(self, itemKey)
return self:ItemIsSelected(Table.GetDistinctKey(self._itemKeyLookup, itemKey))
end
--- Sets the setting info.
-- This method is used to have the selected keys of the dropdown automatically correspond with the value of a field in a
-- table. This is useful for dropdowns which are tied directly to settings.
-- @tparam MultiselectionDropdown self The dropdown object
-- @tparam table tbl The table which the field to set belongs to
-- @tparam string key The key into the table to be set based on the dropdown state
-- @treturn MultiselectionDropdown The dropdown object
function MultiselectionDropdown.SetSettingInfo(self, tbl, key)
local directTbl = tbl[key]
assert(type(directTbl) == "table")
-- this function wipes our settingTable, so set the selected items first
self:SetSelectedItemKeys(directTbl)
self._settingTableDirect = directTbl
return self
end
--- Populate the specified table with a list of selected items
-- @tparam MultiselectionDropdown self The dropdown object
-- @tparam table resultTbl The table to populate
function MultiselectionDropdown.GetSelectedItems(self, resultTbl)
for _, item in ipairs(self._items) do
if self:ItemIsSelected(item) then
tinsert(resultTbl, item)
end
end
end
--- Sets the selection text which is shown to summarize the current value.
-- @tparam BaseDropdown self The dropdown object
-- @tparam string noneText The selection text string when none are selected
-- @tparam string partialText The selection text string for a partial selection
-- @tparam string allText The selection text string when all are selected
-- @treturn BaseDropdown The dropdown object
function MultiselectionDropdown.SetSelectionText(self, noneText, partialText, allText)
assert(type(partialText) == "string" and type(partialText) == "string" and type(allText) == "string")
self._noneSelectionText = noneText
self._partialSelectionText = partialText
self._allSelectionText = allText
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function MultiselectionDropdown._GetDialogSize(self)
local width, height = self.__super:_GetDialogSize()
width = max(width + 12, 200) -- check icon, and big enough for select all / deselect all buttons
height = height + 26 -- header + line
return width, height
end
function MultiselectionDropdown._GetNumSelected(self)
local num = 0
for _, item in ipairs(self._items) do
if self:ItemIsSelected(item) then
num = num + 1
end
end
return num
end
function MultiselectionDropdown._AddDialogChildren(self, frame)
local numSelected = self:_GetNumSelected()
frame:AddChild(UIElements.New("Frame", "header")
:SetLayout("HORIZONTAL")
:SetPadding(8, 8, 2, 2)
:SetHeight(24)
:AddChild(UIElements.New("Button", "selectAll")
:SetWidth("AUTO")
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY2_BOLD")
:SetTextColor(numSelected == #self._items and "ACTIVE_BG_ALT" or "TEXT")
:SetDisabled(numSelected == #self._items)
:SetText(L["Select All"])
:SetScript("OnClick", private.SelectAllOnClick)
)
:AddChild(UIElements.New("Button", "deselectAll")
:SetWidth("AUTO")
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY2_BOLD")
:SetTextColor(numSelected == 0 and "ACTIVE_BG_ALT" or "TEXT")
:SetDisabled(numSelected == 0)
:SetText(L["Deselect All"])
:SetScript("OnClick", private.DeselectAllOnClick)
)
:AddChild(UIElements.New("Spacer", "spacer"))
)
frame:AddChild(UIElements.New("Texture", "line")
:SetHeight(2)
:SetTexture("ACTIVE_BG_ALT")
)
frame:AddChild(UIElements.New("DropdownList", "list")
:SetMultiselect(true)
:SetItems(self._items, self._itemIsSelected)
)
end
function MultiselectionDropdown._GetCurrentSelectionString(self)
local numSelected = self:_GetNumSelected()
local result = nil
if numSelected == 0 then
result = self._hintText ~= "" and self._hintText or self._noneSelectionText
elseif numSelected == #self._items then
result = self._allSelectionText.." ("..numSelected..")"
else
result = format(self._partialSelectionText, numSelected)
end
return result
end
function MultiselectionDropdown._OnListSelectionChanged(self, dropdownList, selection)
self:SetSelectedItems(selection)
local numSelected = self:_GetNumSelected()
dropdownList:GetElement("__parent.header.selectAll")
:SetTextColor(numSelected == #self._items and "ACTIVE_BG_ALT" or "TEXT")
:SetDisabled(numSelected == #self._items)
:Draw()
dropdownList:GetElement("__parent.header.deselectAll")
:SetTextColor(numSelected == 0 and "ACTIVE_BG_ALT" or "TEXT")
:SetDisabled(numSelected == 0)
:Draw()
end
function MultiselectionDropdown._SetItemSelectedHelper(self, item, selected)
self._itemIsSelected[item] = selected and true or nil
if self._settingTableDirect then
self._settingTableDirect[self._itemKeyLookup[item]] = self._itemIsSelected[item]
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.SelectAllOnClick(button)
button:GetElement("__parent.__parent.list"):SelectAll()
end
function private.DeselectAllOnClick(button)
button:GetElement("__parent.__parent.list"):DeselectAll()
end

View File

@@ -0,0 +1,183 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- My Auctions Scrolling Table Class.
-- A scrolling table containing the player's auctions. It is a subclass of the @{QueryScrollingTable} class.
-- @classmod MyAuctionsScrollingTable
local _, TSM = ...
local MyAuctionsScrollingTable = TSM.Include("LibTSMClass").DefineClass("MyAuctionsScrollingTable", TSM.UI.QueryScrollingTable)
local TempTable = TSM.Include("Util.TempTable")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(MyAuctionsScrollingTable)
TSM.UI.MyAuctionsScrollingTable = MyAuctionsScrollingTable
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function MyAuctionsScrollingTable.__init(self)
self.__super:__init()
self._selectionSortValue = nil
self._selectionAuctionId = nil
end
function MyAuctionsScrollingTable.Release(self)
self._selectionSortValue = nil
self._selectionAuctionId = nil
self.__super:Release()
end
--- Sets the selected record.
-- @tparam MyAuctionsScrollingTable self The my auctions scrolling table object
-- @param selection The selected record or nil to clear the selection
-- @tparam[opt=false] bool redraw Whether or not to redraw the scrolling table
-- @treturn MyAuctionsScrollingTable The my auctions scrolling table object
function MyAuctionsScrollingTable.SetSelection(self, selection, redraw)
self.__super:SetSelection(selection, redraw)
if self._selection then
local selectedRow = self:GetSelection()
local sortField = TSM.IsWowClassic() and "index" or self._tableInfo:_GetSortFieldById(self._sortCol)
self._selectionSortValue = selectedRow:GetField(sortField)
if type(self._selectionSortValue) == "string" then
self._selectionSortValue = strlower(self._selectionSortValue)
end
self._selectionAuctionId = selectedRow:GetField("auctionId")
else
self._selectionSortValue = nil
self._selectionAuctionId = nil
end
return self
end
--- Selects the next row.
-- @tparam MyAuctionsScrollingTable self The my auctions scrolling table object
-- @treturn MyAuctionsScrollingTable The my auctions scrolling table object
function MyAuctionsScrollingTable.SelectNextRow(self)
local newSelection = nil
for i = 1, #self._data - 1 do
if self._data[i] == self._selection then
for j = i + 1, #self._data do
if not self._selectionValidator or self:_selectionValidator(self._query:GetResultRowByUUID(self._data[j])) then
newSelection = self._data[j]
break
end
end
break
end
end
self:SetSelection(newSelection, true)
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function MyAuctionsScrollingTable._UpdateSortFromQuery(self)
if self._tableInfo:_IsSortEnabled() then
assert(not TSM.IsWowClassic())
local sortField, sortAscending = self._query:GetOrderBy(2)
if sortField then
self._sortCol = self._tableInfo:_GetIdBySortField(sortField)
self._sortAscending = sortAscending
else
self._sortCol = nil
self._sortAscending = nil
end
end
end
function MyAuctionsScrollingTable._UpdateData(self)
-- we need to fix up the data within the rows updated to avoid errors with trying to access old DatabaseQueryResultRows
local prevRowIndex = TempTable.Acquire()
local newRowData = TempTable.Acquire()
for i, row in ipairs(self._rows) do
if row:IsVisible() then
prevRowIndex[row:GetData()] = i
end
end
local prevSelection = self._selection
wipe(self._data)
self._selection = nil
for _, uuid in self._query:UUIDIterator() do
local row = self._query:GetResultRowByUUID(uuid)
if (uuid == prevSelection or (row:GetField("auctionId") == self._selectionAuctionId)) and not row:GetField("isPending") then
self._selection = uuid
end
if prevRowIndex[uuid] then
newRowData[prevRowIndex[uuid]] = uuid
end
tinsert(self._data, uuid)
end
for i, row in ipairs(self._rows) do
if row:IsVisible() then
if newRowData[i] then
row:SetData(newRowData[i])
else
row:ClearData()
end
end
end
TempTable.Release(prevRowIndex)
TempTable.Release(newRowData)
if prevSelection and not self._selection then
local newSelection = nil
-- try to select the next row based on the sorting
local sortField = TSM.IsWowClassic() and "index" or self._tableInfo:_GetSortFieldById(self._sortCol)
local sortAscending = not TSM.IsWowClassic() and self._sortAscending
for _, uuid in ipairs(self._data) do
local row = self._query:GetResultRowByUUID(uuid)
local sortValue = row:GetField(sortField)
if type(sortValue) == "string" then
sortValue = strlower(sortValue)
end
if (sortAscending and sortValue > self._selectionSortValue) or (not sortAscending and sortValue < self._selectionSortValue) then
if not self._selectionValidator or self:_selectionValidator(row) then
newSelection = uuid
break
end
elseif not TSM.IsWowClassic() and sortValue == self._selectionSortValue and row:GetField("auctionId") > self._selectionAuctionId then
if not self._selectionValidator or self:_selectionValidator(row) then
newSelection = uuid
break
end
end
end
-- select either the next row
self:SetSelection(newSelection)
end
if self._onDataUpdated then
self:_onDataUpdated()
end
end
function MyAuctionsScrollingTable._ToggleSort(self, id)
local sortField = self._tableInfo:_GetSortFieldById(id)
if not self._sortCol or not self._query or not sortField then
-- sorting disabled so ignore
return
end
if id == self._sortCol then
self._sortAscending = not self._sortAscending
else
self._sortCol = id
self._sortAscending = true
end
assert(not TSM.IsWowClassic())
self._query:ResetOrderBy()
:OrderBy("saleStatus", false)
:OrderBy(sortField, self._sortAscending)
:OrderBy("auctionId", true)
self:_UpdateData()
self:Draw()
end

View File

@@ -0,0 +1,288 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- OperationTree UI Element Class.
-- The operation tree is used to display operations grouped by module and allows for adding, duplicating, and deleting
-- them. Only one module is allowed to be expanded at a time. It is a subclass of the @{ScrollingTable} class.
-- @classmod OperationTree
local _, TSM = ...
local OperationTree = TSM.Include("LibTSMClass").DefineClass("OperationTree", TSM.UI.ScrollingTable)
local L = TSM.Include("Locale").GetTable()
local Theme = TSM.Include("Util.Theme")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(OperationTree)
TSM.UI.OperationTree = OperationTree
local private = {}
local DATA_SEP = "\001"
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function OperationTree.__init(self)
self.__super:__init()
self:SetRowHeight(28)
self._operationNameFilter = ""
self._selected = nil
self._expandedModule = nil
self._selectedOperation = nil
self._prevSelectedOperation = nil
self._onOperationSelectedHandler = nil
self._onOperationAddedHandler = nil
self._onOperationDeletedHandler = nil
end
function OperationTree.Acquire(self)
self._backgroundColor = "PRIMARY_BG_ALT"
self._headerHidden = true
self.__super:Acquire()
self:GetScrollingTableInfo()
:NewColumn("text")
:SetFont("BODY_BODY2_MEDIUM")
:SetJustifyH("LEFT")
:SetTextFunction(private.GetText)
:SetExpanderStateFunction(private.GetExpanderState)
:SetActionIconInfo(2, 14, private.GetActionIcon)
:SetActionIconClickHandler(private.OnActionIconClick)
:DisableHiding()
:Commit()
:Commit()
self:UpdateData()
end
function OperationTree.Release(self)
for _, row in ipairs(self._rows) do
ScriptWrapper.Clear(row._frame, "OnDoubleClick")
end
self._selected = nil
self._operationNameFilter = ""
self._expandedModule = nil
self._selectedOperation = nil
self._prevSelectedOperation = nil
self._onOperationSelectedHandler = nil
self._onOperationAddedHandler = nil
self._onOperationDeletedHandler = nil
self.__super:Release()
self:SetRowHeight(28)
end
--- Sets the operation name filter.
-- @tparam OperationTree self The operation tree object
-- @tparam string filter The filter string (any operations which don't match this are hidden)
function OperationTree.SetOperationNameFilter(self, filter)
self._operationNameFilter = filter
if filter == "" and self._prevSelectedOperation and not self._selectedOperation then
-- restore any previous selection if we don't have something selected
self:SetSelectedOperation(self:_SplitOperationKey(self._prevSelectedOperation))
self._prevSelectedOperation = nil
elseif filter ~= "" and self._selectedOperation then
local _, operationName = self:_SplitOperationKey(self._selectedOperation)
if not operationName or not strmatch(strlower(operationName), filter) then
-- save the current selection to restore after the filter is cleared and then clear the current selection
self._prevSelectedOperation = self._selectedOperation
self:SetSelectedOperation()
end
end
self:UpdateData(true)
end
--- Registers a script handler.
-- @tparam OperationTree self The operation tree object
-- @tparam string script The script to register for (supported scripts: `OnOperationSelected`, `OnOperationAdded`,
-- `OnOperationDeleted`)
-- @tparam function handler The script handler which will be called with the operation tree object followed by any
-- arguments to the script
-- @treturn OperationTree The operation tree object
function OperationTree.SetScript(self, script, handler)
if script == "OnOperationSelected" then
self._onOperationSelectedHandler = handler
elseif script == "OnOperationAdded" then
self._onOperationAddedHandler = handler
elseif script == "OnOperationDeleted" then
self._onOperationDeletedHandler = handler
else
error("Unknown OperationTree script: "..tostring(script))
end
return self
end
--- Sets the selected operation.
-- @tparam OperationTree self The operation tree object
-- @tparam string moduleName The name of the module which the operation belongs to
-- @tparam string operationName The name of the operation
-- @treturn OperationTree The operation tree object
function OperationTree.SetSelectedOperation(self, moduleName, operationName)
if moduleName and operationName then
self._selectedOperation = moduleName..DATA_SEP..operationName
self._expandedModule = moduleName
elseif moduleName then
self._selectedOperation = moduleName
self._expandedModule = moduleName
else
self._selectedOperation = nil
self._expandedModule = nil
end
self:UpdateData()
self.__super:SetSelection(self._selectedOperation, true)
if self._onOperationSelectedHandler then
self:_onOperationSelectedHandler(moduleName, operationName)
end
self:_ForceLastDataUpdate()
self:UpdateData(true)
return self
end
function OperationTree.SetSelection(self, data)
self:SetSelectedOperation(self:_SplitOperationKey(data))
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function OperationTree._GetTableRow(self, isHeader)
local row = self.__super:_GetTableRow(isHeader)
if not isHeader then
ScriptWrapper.Set(row._frame, "OnDoubleClick", private.RowOnDoubleClick, row)
end
return row
end
function OperationTree._IsDataHidden(self, data)
local moduleName, operationName = self:_SplitOperationKey(data)
if operationName and not strmatch(strlower(operationName), self._operationNameFilter) then
return true
elseif operationName and moduleName ~= self._expandedModule then
return true
end
return false
end
function OperationTree._SplitOperationKey(self, data)
local moduleName, operationName = strmatch(data, "([^"..DATA_SEP.."]+)"..DATA_SEP.."?(.*)")
operationName = operationName ~= "" and operationName or nil
return moduleName, operationName
end
function OperationTree._UpdateData(self)
wipe(self._data)
for _, moduleName in TSM.Operations.ModuleIterator() do
if not self:_IsDataHidden(moduleName) then
tinsert(self._data, moduleName)
end
for _, operationName in TSM.Operations.OperationIterator(moduleName) do
local data = moduleName..DATA_SEP..operationName
if not self:_IsDataHidden(data) then
tinsert(self._data, data)
end
end
end
end
function OperationTree._HandleRowClick(self)
self:Draw()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.GetText(self, data)
local moduleName, operationName = self:_SplitOperationKey(data)
local color = Theme.GetColor(operationName and "TEXT" or "INDICATOR")
return color:ColorText(operationName or moduleName.." "..L["Operations"])
end
function private.GetExpanderState(self, data)
local moduleName, operationName = self:_SplitOperationKey(data)
return not operationName, self._expandedModule == moduleName, operationName and 1 or 0
end
function private.GetActionIcon(self, data, iconIndex, isMouseOver)
local _, operationName = self:_SplitOperationKey(data)
if iconIndex == 1 then
if operationName and data == self._selectedOperation then
local texturePack = "iconPack.14x14/Duplicate"
return true, isMouseOver and TSM.UI.TexturePacks.GetColoredKey(texturePack, Theme.GetColor("INDICATOR")) or texturePack
elseif operationName then
return false, nil
else
local texturePack = "iconPack.14x14/Add/Circle"
return true, isMouseOver and TSM.UI.TexturePacks.GetColoredKey(texturePack, Theme.GetColor("INDICATOR")) or texturePack
end
elseif iconIndex == 2 then
if operationName and data == self._selectedOperation then
local texturePack = "iconPack.14x14/Delete"
return true, isMouseOver and TSM.UI.TexturePacks.GetColoredKey(texturePack, Theme.GetColor("INDICATOR")) or texturePack
else
return false, nil
end
else
error("Invalid index: "..tostring(iconIndex))
end
end
function private.OnActionIconClick(self, data, iconIndex)
local moduleName, operationName = self:_SplitOperationKey(data)
if iconIndex == 1 then
if operationName then
-- duplicate
local num = 1
while TSM.Operations.Exists(moduleName, operationName.." "..num) do
num = num + 1
end
local newOperationName = operationName.." "..num
self:_onOperationAddedHandler(moduleName, newOperationName, operationName)
self:UpdateData()
self:SetSelectedOperation(moduleName, newOperationName)
else
-- add
operationName = "New Operation"
local num = 1
while TSM.Operations.Exists(moduleName, operationName.." "..num) do
num = num + 1
end
operationName = operationName .. " " .. num
self._expandedModule = moduleName
self:_onOperationAddedHandler(moduleName, operationName)
self:UpdateData()
self:SetSelectedOperation(moduleName, operationName)
end
self:Draw()
elseif iconIndex == 2 then
assert(operationName)
-- delete
self:_onOperationDeletedHandler(moduleName, operationName)
self:UpdateData(true)
else
error("Invalid index: "..tostring(iconIndex))
end
end
function private.RowOnDoubleClick(row, mouseButton)
if mouseButton ~= "LeftButton" then
return
end
local self = row._scrollingTable
local data = row:GetData()
local moduleName, operationName = self:_SplitOperationKey(data)
if operationName then
return
end
if moduleName == self._selectedOperation then
self:SetSelectedOperation()
else
self:SetSelectedOperation(moduleName, operationName)
end
end

View File

@@ -0,0 +1,186 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- OverlayApplicationFrame UI Element Class.
-- The overlay application frame is currently just used for the TaskListUI. It is a subclass of the @{Frame} class.
-- @classmod OverlayApplicationFrame
local _, TSM = ...
local Theme = TSM.Include("Util.Theme")
local UIElements = TSM.Include("UI.UIElements")
local OverlayApplicationFrame = TSM.Include("LibTSMClass").DefineClass("OverlayApplicationFrame", TSM.UI.Frame)
UIElements.Register(OverlayApplicationFrame)
TSM.UI.OverlayApplicationFrame = OverlayApplicationFrame
local private = {}
local TITLE_HEIGHT = 40
local CONTENT_PADDING_BOTTOM = 16
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function OverlayApplicationFrame.__init(self)
self.__super:__init()
self._contentFrame = nil
self._contextTable = nil
self._defaultContextTable = nil
Theme.RegisterChangeCallback(function()
if self:IsVisible() then
self:Draw()
end
end)
end
function OverlayApplicationFrame.Acquire(self)
local frame = self:_GetBaseFrame()
frame:EnableMouse(true)
frame:SetMovable(true)
frame:RegisterForDrag("LeftButton")
self:AddChildNoLayout(UIElements.New("Button", "closeBtn")
:AddAnchor("TOPRIGHT", -8, -11)
:SetBackgroundAndSize("iconPack.18x18/Close/Circle")
:SetScript("OnClick", private.CloseButtonOnClick)
)
self:AddChildNoLayout(UIElements.New("Button", "minimizeBtn")
:AddAnchor("TOPRIGHT", -26, -11)
:SetBackgroundAndSize("iconPack.18x18/Subtract/Circle")
:SetScript("OnClick", private.MinimizeBtnOnClick)
)
self:AddChildNoLayout(UIElements.New("Text", "title")
:SetHeight(24)
:SetFont("BODY_BODY1_BOLD")
:AddAnchor("TOPLEFT", 8, -8)
:AddAnchor("TOPRIGHT", -52, -8)
)
self:SetScript("OnDragStart", private.FrameOnDragStart)
self:SetScript("OnDragStop", private.FrameOnDragStop)
self.__super:Acquire()
end
function OverlayApplicationFrame.Release(self)
self._contentFrame = nil
self._contextTable = nil
self._defaultContextTable = nil
self:_GetBaseFrame():SetMinResize(0, 0)
self.__super:Release()
end
--- Sets the title text.
-- @tparam OverlayApplicationFrame self The overlay application frame object
-- @tparam string title The title text
-- @treturn OverlayApplicationFrame The overlay application frame object
function OverlayApplicationFrame.SetTitle(self, title)
self:GetElement("title"):SetText(title)
return self
end
--- Sets the content frame.
-- @tparam OverlayApplicationFrame self The overlay application frame object
-- @tparam Element frame The content frame
-- @treturn OverlayApplicationFrame The overlay application frame object
function OverlayApplicationFrame.SetContentFrame(self, frame)
frame:WipeAnchors()
frame:AddAnchor("TOPLEFT", 0, -TITLE_HEIGHT)
frame:AddAnchor("BOTTOMRIGHT", 0, CONTENT_PADDING_BOTTOM)
self._contentFrame = frame
self:AddChildNoLayout(frame)
return self
end
--- Sets the context table.
-- This table can be used to preserve position information across lifecycles of the frame and even WoW sessions if it's
-- within the settings DB.
-- @tparam OverlayApplicationFrame self The overlay application frame object
-- @tparam table tbl The context table
-- @tparam table defaultTbl The default values (required fields: `minimized`, `topRightX`, `topRightY`)
-- @treturn OverlayApplicationFrame The overlay application frame object
function OverlayApplicationFrame.SetContextTable(self, tbl, defaultTbl)
assert(defaultTbl.minimized ~= nil and defaultTbl.topRightX and defaultTbl.topRightY)
if tbl.minimized == nil then
tbl.minimized = defaultTbl.minimized
end
tbl.topRightX = tbl.topRightX or defaultTbl.topRightX
tbl.topRightY = tbl.topRightY or defaultTbl.topRightY
self._contextTable = tbl
self._defaultContextTable = defaultTbl
return self
end
--- Sets the context table from a settings object.
-- @tparam OverlayApplicationFrame self The overlay application frame object
-- @tparam Settings settings The settings object
-- @tparam string key The setting key
-- @treturn OverlayApplicationFrame The overlay application frame object
function OverlayApplicationFrame.SetSettingsContext(self, settings, key)
return self:SetContextTable(settings[key], settings:GetDefaultReadOnly(key))
end
function OverlayApplicationFrame.Draw(self)
if self._contextTable.minimized then
self:GetElement("minimizeBtn"):SetBackgroundAndSize("iconPack.18x18/Add/Circle")
self:GetElement("content"):Hide()
self:SetHeight(TITLE_HEIGHT)
else
self:GetElement("minimizeBtn"):SetBackgroundAndSize("iconPack.18x18/Subtract/Circle")
self:GetElement("content"):Show()
-- set the height of the frame based on the height of the children
local contentHeight, contentHeightExpandable = self:GetElement("content"):_GetMinimumDimension("HEIGHT")
assert(not contentHeightExpandable)
self:SetHeight(contentHeight + TITLE_HEIGHT + CONTENT_PADDING_BOTTOM)
end
-- make sure the frame is on the screen
self._contextTable.topRightX = max(min(self._contextTable.topRightX, 0), -UIParent:GetWidth() + 100)
self._contextTable.topRightY = max(min(self._contextTable.topRightY, 0), -UIParent:GetHeight() + 100)
-- set the frame position from the contextTable
self:WipeAnchors()
self:AddAnchor("TOPRIGHT", self._contextTable.topRightX, self._contextTable.topRightY)
self.__super:Draw()
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function OverlayApplicationFrame._SavePosition(self)
local frame = self:_GetBaseFrame()
local parentFrame = frame:GetParent()
self._contextTable.topRightX = frame:GetRight() - parentFrame:GetRight()
self._contextTable.topRightY = frame:GetTop() - parentFrame:GetTop()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.FrameOnDragStart(self)
self:_GetBaseFrame():StartMoving()
end
function private.FrameOnDragStop(self)
local frame = self:_GetBaseFrame()
frame:StopMovingOrSizing()
self:_SavePosition()
end
function private.CloseButtonOnClick(button)
button:GetParentElement():Hide()
end
function private.MinimizeBtnOnClick(button)
local self = button:GetParentElement()
self._contextTable.minimized = not self._contextTable.minimized
self:Draw()
end

View File

@@ -0,0 +1,103 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- PlayerGoldText UI Element Class.
-- A text element which contains player gold info which automatically updates when the player's gold amount changes. It
-- is a subclass of the @{Text} class.
-- @classmod PlayerGoldText
local _, TSM = ...
local PlayerGoldText = TSM.Include("LibTSMClass").DefineClass("PlayerGoldText", TSM.UI.Text)
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
local Event = TSM.Include("Util.Event")
local Money = TSM.Include("Util.Money")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(PlayerGoldText)
TSM.UI.PlayerGoldText = PlayerGoldText
local private = {
registered = false,
elements = {},
}
-- ============================================================================
-- Meta Class Methods
-- ============================================================================
function PlayerGoldText.__init(self)
self.__super:__init()
self:_GetBaseFrame():EnableMouse(true)
if not private.registered then
Event.Register("PLAYER_MONEY", private.MoneyOnUpdate)
private.registered = true
end
self._justifyH = "RIGHT"
self._font = "TABLE_TABLE1"
end
function PlayerGoldText.Acquire(self)
private.elements[self] = true
self.__super:Acquire()
self:SetText(Money.ToString(TSM.db.global.appearanceOptions.showTotalMoney and private.GetTotalMoney() or GetMoney()))
self:SetTooltip(private.MoneyTooltipFunc)
end
function PlayerGoldText.Release(self)
private.elements[self] = nil
self.__super:Release()
self._justifyH = "RIGHT"
self._font = "TABLE_TABLE1"
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.MoneyOnUpdate()
for element in pairs(private.elements) do
element:SetText(Money.ToString(TSM.db.global.appearanceOptions.showTotalMoney and private.GetTotalMoney() or GetMoney()))
element:Draw()
end
end
function private.MoneyTooltipFunc()
local tooltipLines = TempTable.Acquire()
local playerMoney = TSM.db.sync.internalData.money
local total = playerMoney
tinsert(tooltipLines, strjoin(TSM.CONST.TOOLTIP_SEP, UnitName("player")..":", Money.ToString(playerMoney)))
local numPosted, numSold, postedGold, soldGold = TSM.MyAuctions.GetAuctionInfo()
if numPosted then
tinsert(tooltipLines, " "..strjoin(TSM.CONST.TOOLTIP_SEP, format(L["%s Sold Auctions"], numSold)..":", Money.ToString(soldGold)))
tinsert(tooltipLines, " "..strjoin(TSM.CONST.TOOLTIP_SEP, format(L["%s Posted Auctions"], numPosted)..":", Money.ToString(postedGold)))
end
for _, _, character, syncScopeKey in Settings.ConnectedFactionrealmAltCharacterIterator() do
local money = Settings.Get("sync", syncScopeKey, "internalData", "money")
if money > 0 then
tinsert(tooltipLines, strjoin(TSM.CONST.TOOLTIP_SEP, character..":", Money.ToString(money)))
total = total + money
end
end
tinsert(tooltipLines, 1, strjoin(TSM.CONST.TOOLTIP_SEP, L["Total Gold"]..":", Money.ToString(total)))
return strjoin("\n", TempTable.UnpackAndRelease(tooltipLines))
end
function private.GetTotalMoney()
local total = TSM.db.sync.internalData.money
for _, _, _, syncScopeKey in Settings.ConnectedFactionrealmAltCharacterIterator() do
local money = Settings.Get("sync", syncScopeKey, "internalData", "money")
if money > 0 then
total = total + money
end
end
return total
end

View File

@@ -0,0 +1,35 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- PopupFrame UI Element Class.
-- A popup frame which shows when clicking on a "more" button.
-- @classmod PopupFrame
local _, TSM = ...
local NineSlice = TSM.Include("Util.NineSlice")
local Theme = TSM.Include("Util.Theme")
local PopupFrame = TSM.Include("LibTSMClass").DefineClass("PopupFrame", TSM.UI.Frame)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(PopupFrame)
TSM.UI.PopupFrame = PopupFrame
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function PopupFrame.__init(self)
self.__super:__init()
self._nineSlice = NineSlice.New(self:_GetBaseFrame())
end
function PopupFrame.Draw(self)
self.__super:Draw()
self._nineSlice:SetStyle("popup")
-- TOOD: fix the texture color properly
self._nineSlice:SetPartVertexColor("center", Theme.GetColor("PRIMARY_BG_ALT", "duskwood"):GetFractionalRGBA())
end

View File

@@ -0,0 +1,615 @@
-- ------------------------------------------------------------------------------ --
-- 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

View File

@@ -0,0 +1,168 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- ProgressBar UI Element Class.
-- The progress bar element is a left-to-right progress bar with an anaimated progress indicator and text. It is a
-- subclass of the @{Text} class.
-- @classmod ProgressBar
local _, TSM = ...
local Theme = TSM.Include("Util.Theme")
local UIElements = TSM.Include("UI.UIElements")
local ProgressBar = TSM.Include("LibTSMClass").DefineClass("ProgressBar", TSM.UI.Text)
UIElements.Register(ProgressBar)
TSM.UI.ProgressBar = ProgressBar
local PROGRESS_PADDING = 2
local PROGRESS_ICON_PADDING = 4
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ProgressBar.__init(self)
local frame = UIElements.CreateFrame(self, "Frame")
self.__super:__init(frame)
self._bgLeft = frame:CreateTexture(nil, "BACKGROUND")
self._bgLeft:SetPoint("TOPLEFT")
self._bgLeft:SetPoint("BOTTOMLEFT")
TSM.UI.TexturePacks.SetTextureAndWidth(self._bgLeft, "uiFrames.LoadingBarLeft")
self._bgRight = frame:CreateTexture(nil, "BACKGROUND")
self._bgRight:SetPoint("TOPRIGHT")
self._bgRight:SetPoint("BOTTOMRIGHT")
TSM.UI.TexturePacks.SetTextureAndWidth(self._bgRight, "uiFrames.LoadingBarRight")
self._bgMiddle = frame:CreateTexture(nil, "BACKGROUND")
self._bgMiddle:SetPoint("TOPLEFT", self._bgLeft, "TOPRIGHT")
self._bgMiddle:SetPoint("BOTTOMRIGHT", self._bgRight, "BOTTOMLEFT")
TSM.UI.TexturePacks.SetTexture(self._bgMiddle, "uiFrames.LoadingBarMiddle")
-- create the progress textures
self._progressLeft = frame:CreateTexture(nil, "ARTWORK")
self._progressLeft:SetPoint("TOPLEFT", PROGRESS_PADDING, -PROGRESS_PADDING)
self._progressLeft:SetPoint("BOTTOMLEFT", PROGRESS_PADDING, PROGRESS_PADDING)
self._progressLeft:SetBlendMode("BLEND")
TSM.UI.TexturePacks.SetTexture(self._progressLeft, "uiFrames.LoadingBarLeft")
self._progressMiddle = frame:CreateTexture(nil, "ARTWORK")
self._progressMiddle:SetPoint("TOPLEFT", self._progressLeft, "TOPRIGHT")
self._progressMiddle:SetPoint("BOTTOMLEFT", self._progressLeft, "BOTTOMRIGHT")
self._progressMiddle:SetBlendMode("BLEND")
TSM.UI.TexturePacks.SetTexture(self._progressMiddle, "uiFrames.LoadingBarMiddle")
self._progressRight = frame:CreateTexture(nil, "ARTWORK")
self._progressRight:SetPoint("TOPLEFT", self._progressMiddle, "TOPRIGHT")
self._progressRight:SetPoint("BOTTOMLEFT", self._progressMiddle, "BOTTOMRIGHT")
self._progressRight:SetBlendMode("BLEND")
TSM.UI.TexturePacks.SetTexture(self._progressRight, "uiFrames.LoadingBarRight")
-- create the progress icon
frame.progressIcon = frame:CreateTexture(nil, "OVERLAY")
frame.progressIcon:SetPoint("RIGHT", frame.text, "LEFT", -PROGRESS_ICON_PADDING, 0)
frame.progressIcon:Hide()
frame.progressIcon.ag = frame.progressIcon:CreateAnimationGroup()
local spin = frame.progressIcon.ag:CreateAnimation("Rotation")
spin:SetDuration(2)
spin:SetDegrees(360)
frame.progressIcon.ag:SetLooping("REPEAT")
self._progress = 0
self._progressIconHidden = false
self._justifyH = "CENTER"
self._font = "BODY_BODY2_MEDIUM"
self._textColor = "INDICATOR"
end
function ProgressBar.Release(self)
self._progress = 0
self._progressIconHidden = false
self:_GetBaseFrame().progressIcon.ag:Stop()
self:_GetBaseFrame().progressIcon:Hide()
self.__super:Release()
self._justifyH = "CENTER"
self._font = "BODY_BODY2_MEDIUM"
self._textColor = "INDICATOR"
end
--- Sets the progress.
-- @tparam ProgressBar self The progress bar object
-- @tparam number progress The progress from a value of 0 to 1 (inclusive)
-- @tparam boolean isDone Whether or not the progress is finished
-- @treturn ProgressBar The progress bar object
function ProgressBar.SetProgress(self, progress, isDone)
self._progress = progress
return self
end
--- Sets whether or not the progress indicator is hidden.
-- @tparam ProgressBar self The progress bar object
-- @tparam boolean hidden Whether or not the progress indicator is hidden
-- @treturn ProgressBar The progress bar object
function ProgressBar.SetProgressIconHidden(self, hidden)
self._progressIconHidden = hidden
return self
end
function ProgressBar.Draw(self)
self.__super:Draw()
local frame = self:_GetBaseFrame()
self._bgLeft:SetVertexColor(Theme.GetColor("PRIMARY_BG"):GetFractionalRGBA())
self._bgMiddle:SetVertexColor(Theme.GetColor("PRIMARY_BG"):GetFractionalRGBA())
self._bgRight:SetVertexColor(Theme.GetColor("PRIMARY_BG"):GetFractionalRGBA())
local text = frame.text
text:ClearAllPoints()
text:SetWidth(self:_GetDimension("WIDTH"))
text:SetWidth(frame.text:GetStringWidth())
text:SetHeight(self:_GetDimension("HEIGHT"))
text:SetPoint("CENTER", self._progressIconHidden and 0 or ((TSM.UI.TexturePacks.GetWidth("iconPack.18x18/Running") + PROGRESS_ICON_PADDING) / 2), 0)
TSM.UI.TexturePacks.SetTextureAndSize(frame.progressIcon, "iconPack.18x18/Running")
frame.progressIcon:SetVertexColor(self:_GetTextColor():GetFractionalRGBA())
if self._progressIconHidden and frame.progressIcon:IsVisible() then
frame.progressIcon.ag:Stop()
frame.progressIcon:Hide()
elseif not self._progressIconHidden and not frame.progressIcon:IsVisible() then
frame.progressIcon:Show()
frame.progressIcon.ag:Play()
end
if self._progress == 0 then
self._progressLeft:Hide()
self._progressMiddle:Hide()
self._progressRight:Hide()
else
self._progressLeft:Show()
self._progressMiddle:Show()
self._progressRight:Show()
self._progressLeft:SetVertexColor(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
self._progressMiddle:SetVertexColor(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
self._progressRight:SetVertexColor(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
local leftTextureWidth = TSM.UI.TexturePacks.GetWidth("uiFrames.LoadingBarLeft")
local rightTextureWidth = TSM.UI.TexturePacks.GetWidth("uiFrames.LoadingBarRight")
local maxProgressWidth = self:_GetDimension("WIDTH") - PROGRESS_PADDING * 2
local progressWidth = maxProgressWidth * self._progress
if progressWidth <= leftTextureWidth then
self._progressLeft:SetWidth(progressWidth)
self._progressMiddle:Hide()
self._progressRight:Hide()
elseif progressWidth < maxProgressWidth - rightTextureWidth then
self._progressLeft:SetWidth(leftTextureWidth)
self._progressMiddle:SetWidth(progressWidth - leftTextureWidth)
self._progressRight:Hide()
else
self._progressLeft:SetWidth(leftTextureWidth)
self._progressMiddle:SetWidth(progressWidth - leftTextureWidth - rightTextureWidth)
self._progressRight:SetWidth(rightTextureWidth)
end
end
end

View File

@@ -0,0 +1,258 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Query Scrolling Table UI ScrollingTable Class.
-- A query scrolling table contains a scrollable list of rows with a fixed set of columns. It is a subclass of the
-- @{ScrollingTable} class.
-- @classmod QueryScrollingTable
local _, TSM = ...
local QueryScrollingTable = TSM.Include("LibTSMClass").DefineClass("QueryScrollingTable", TSM.UI.ScrollingTable)
local TempTable = TSM.Include("Util.TempTable")
local Table = TSM.Include("Util.Table")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(QueryScrollingTable)
TSM.UI.QueryScrollingTable = QueryScrollingTable
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function QueryScrollingTable.__init(self)
self.__super:__init()
self._query = nil
self._sortCol = nil
self._sortAscending = nil
self._autoReleaseQuery = false
end
function QueryScrollingTable.Release(self)
if self._query then
self._query:SetUpdateCallback()
if self._autoReleaseQuery then
self._query:Release()
end
self._query = nil
end
self._sortCol = nil
self._sortAscending = nil
self._autoReleaseQuery = false
self.__super:Release()
end
--- Sets the @{DatabaseQuery} source for this table.
-- This query is used to populate the entries in the query scrolling table.
-- @tparam QueryScrollingTable self The query scrolling table object
-- @tparam DatabaseQuery query The query object
-- @tparam[opt=false] bool redraw Whether or not to redraw the scrolling table
-- @treturn QueryScrollingTable The query scrolling table object
function QueryScrollingTable.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:_UpdateSortFromQuery()
self:_ForceLastDataUpdate()
self:UpdateData(redraw)
return self
end
--- Sets whether or not the @{DatabaseQuery} is automatically released.
-- @tparam QueryScrollingTable self The query scrolling table object
-- @tparam bool autoRelease Whether or not to auto-release the query
-- @treturn QueryScrollingTable The query scrolling table object
function QueryScrollingTable.SetAutoReleaseQuery(self, autoRelease)
self._autoReleaseQuery = autoRelease
return self
end
--- Sets the selected record.
-- @tparam QueryScrollingTable self The query scrolling table object
-- @param selection The selected record or nil to clear the selection
-- @tparam[opt=false] bool redraw Whether or not to redraw the scrolling table
-- @treturn QueryScrollingTable The query scrolling table object
function QueryScrollingTable.SetSelection(self, selection, redraw)
if selection == self._selection then
return self
elseif selection and self._selectionValidator and not self:_selectionValidator(self._query:GetResultRowByUUID(selection)) then
return self
end
local index = nil
if selection then
index = Table.KeyByValue(self._data, selection)
assert(index)
end
self:_IgnoreLastDataUpdate()
self._selection = selection
if selection then
-- set the scroll so that the selection is visible if necessary
local rowHeight = self._rowHeight
local firstVisibleIndex = ceil(self._vScrollValue / rowHeight) + 1
local lastVisibleIndex = floor((self._vScrollValue + self:_GetDimension("HEIGHT")) / rowHeight)
if lastVisibleIndex > firstVisibleIndex and (index < firstVisibleIndex or index > lastVisibleIndex) then
self:_OnScrollValueChanged(min((index - 1) * rowHeight, self:_GetMaxScroll()))
end
end
for _, row in ipairs(self._rows) do
if not row:IsMouseOver() and row:IsVisible() and row:GetData() ~= selection then
row:SetHighlightState(nil)
elseif row:IsMouseOver() and row:IsVisible() then
row:SetHighlightState(row:GetData() == selection and "selectedHover" or "hover")
elseif row:IsVisible() and row:GetData() == selection then
row:SetHighlightState("selected")
end
end
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
if redraw then
self:Draw()
end
return self
end
--- Gets the currently selected row.
-- @tparam QueryScrollingTable self The scrolling table object
-- @return The selected row or nil if there's nothing selected
function QueryScrollingTable.GetSelection(self)
return self._selection and self._query:GetResultRowByUUID(self._selection) or nil
end
--- Registers a script handler.
-- @tparam QueryScrollingTable self The scrolling table object
-- @tparam string script The script to register for (supported scripts: `OnDataUpdated`, `OnSelectionChanged`, `OnRowClick`)
-- @tparam function handler The script handler which will be called with the scrolling table object followed by any
-- arguments to the script
-- @treturn QueryScrollingTable The scrolling table object
function QueryScrollingTable.SetScript(self, script, handler)
if script == "OnDataUpdated" then
self._onDataUpdated = handler
else
self.__super:SetScript(script, handler)
end
return self
end
function QueryScrollingTable.Draw(self)
self._query:SetUpdatesPaused(true)
if self._lastDataUpdate == nil then
self:_IgnoreLastDataUpdate()
end
self.__super:Draw()
self._header:SetSort(self._sortCol, self._sortAscending)
self._query:SetUpdatesPaused(false)
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function QueryScrollingTable._CreateScrollingTableInfo(self)
return TSM.UI.Util.QueryScrollingTableInfo()
end
function QueryScrollingTable._UpdateSortFromQuery(self)
if self._tableInfo:_IsSortEnabled() then
local sortField, sortAscending = self._query:GetLastOrderBy()
if sortField then
self._sortCol = self._tableInfo:_GetIdBySortField(sortField)
self._sortAscending = sortAscending
else
self._sortCol = nil
self._sortAscending = nil
end
end
end
function QueryScrollingTable._UpdateData(self)
-- we need to fix up the data within the rows updated to avoid errors with trying to access old DatabaseQueryResultRows
local prevRowIndex = TempTable.Acquire()
local newRowData = TempTable.Acquire()
for i, row in ipairs(self._rows) do
if row:IsVisible() then
prevRowIndex[row:GetData()] = i
end
end
local prevSelection = self._selection
wipe(self._data)
self._selection = nil
for _, uuid in self._query:UUIDIterator() do
if uuid == prevSelection then
self._selection = uuid
end
if prevRowIndex[uuid] then
newRowData[prevRowIndex[uuid]] = uuid
end
tinsert(self._data, uuid)
end
for i, row in ipairs(self._rows) do
if row:IsVisible() then
if newRowData[i] then
row:SetData(newRowData[i])
else
row:ClearData()
end
end
end
TempTable.Release(prevRowIndex)
TempTable.Release(newRowData)
if prevSelection and not self._selection then
-- select the first row since we weren't able to find the previously-selected row
self:SetSelection(self._data[1])
end
if self._onDataUpdated then
self:_onDataUpdated()
end
end
function QueryScrollingTable._ToggleSort(self, id)
local sortField = self._tableInfo:_GetSortFieldById(id)
if not self._sortCol or not self._query or not sortField then
-- sorting disabled so ignore
return
end
if id == self._sortCol then
self._sortAscending = not self._sortAscending
else
self._sortCol = id
self._sortAscending = true
end
self._query:UpdateLastOrderBy(sortField, self._sortAscending)
self:_UpdateData()
self:Draw()
end
function QueryScrollingTable._HandleRowClick(self, uuid, mouseButton)
if self._onRowClickHandler then
self:_onRowClickHandler(self._query:GetResultRowByUUID(uuid), mouseButton)
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.QueryUpdateCallback(_, uuid, self)
self:_SetLastDataUpdate(uuid)
if not uuid then
self:_UpdateData()
end
self:Draw()
end

View File

@@ -0,0 +1,237 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- ScrollFrame UI Element Class.
-- A scroll frame is a container which allows the content to be of unlimited (but fixed/static) height within a
-- scrollable window. It is a subclass of the @{Container} class.
-- @classmod ScrollFrame
local _, TSM = ...
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Theme = TSM.Include("Util.Theme")
local NineSlice = TSM.Include("Util.NineSlice")
local ScrollFrame = TSM.Include("LibTSMClass").DefineClass("ScrollFrame", TSM.UI.Container)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(ScrollFrame)
TSM.UI.ScrollFrame = ScrollFrame
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ScrollFrame.__init(self)
local frame = UIElements.CreateFrame(self, "ScrollFrame")
self.__super:__init(frame)
self._backgroundNineSlice = NineSlice.New(frame, 1)
self._backgroundNineSlice:Hide()
frame:EnableMouseWheel(true)
frame:SetClipsChildren(true)
ScriptWrapper.Set(frame, "OnUpdate", private.FrameOnUpdate, self)
ScriptWrapper.Set(frame, "OnMouseWheel", private.FrameOnMouseWheel, self)
self._scrollbar = TSM.UI.Scrollbar.Create(frame)
ScriptWrapper.Set(self._scrollbar, "OnValueChanged", private.OnScrollbarValueChanged, self)
self._content = CreateFrame("Frame", nil, frame)
self._content:SetPoint("TOPLEFT")
self._content:SetPoint("TOPRIGHT")
frame:SetScrollChild(self._content)
self._scrollValue = 0
self._onUpdateHandler = nil
self._backgroundColor = nil
end
function ScrollFrame.Acquire(self)
self.__super:Acquire()
self._scrollValue = 0
self._scrollbar:SetValue(0)
end
function ScrollFrame.Release(self)
self._onUpdateHandler = nil
self._backgroundColor = nil
self._backgroundNineSlice:Hide()
self.__super:Release()
end
--- Sets the background of the scroll frame.
-- @tparam ScrollFrame self The scroll frame object
-- @tparam ?string|nil color The background color as a theme color key or nil
-- @treturn ScrollFrame The scroll frame object
function ScrollFrame.SetBackgroundColor(self, color)
assert(color == nil or Theme.GetColor(color))
self._backgroundColor = color
return self
end
function ScrollFrame.SetScript(self, script, handler)
if script == "OnUpdate" then
self._onUpdateHandler = handler
else
self.__super:SetScript(script, handler)
end
return self
end
function ScrollFrame.Draw(self)
self.__super.__super:Draw()
if self._backgroundColor then
self._backgroundNineSlice:SetStyle("solid")
self._backgroundNineSlice:SetVertexColor(Theme.GetColor(self._backgroundColor):GetFractionalRGBA())
else
self._backgroundNineSlice:Hide()
end
local width = self:_GetDimension("WIDTH")
self._content:SetWidth(width)
width = width - self:_GetPadding("LEFT") - self:_GetPadding("RIGHT")
local totalHeight = self:_GetPadding("TOP") + self:_GetPadding("BOTTOM")
for _, child in self:LayoutChildrenIterator() do
child:_GetBaseFrame():SetParent(self._content)
child:_GetBaseFrame():ClearAllPoints()
-- set the height
local childHeight, childHeightCanExpand = child:_GetMinimumDimension("HEIGHT")
assert(not childHeightCanExpand, "Invalid height for child: "..tostring(child._id))
child:_SetDimension("HEIGHT", childHeight)
totalHeight = totalHeight + childHeight + child:_GetMargin("TOP") + child:_GetMargin("BOTTOM")
-- set the width
local childWidth, childWidthCanExpand = child:_GetMinimumDimension("WIDTH")
if childWidthCanExpand then
childWidth = max(childWidth, width - child:_GetMargin("LEFT") - child:_GetMargin("RIGHT"))
end
child:_SetDimension("WIDTH", childWidth)
end
self._content:SetHeight(totalHeight)
local maxScroll = self:_GetMaxScroll()
self._scrollbar:SetMinMaxValues(0, maxScroll)
self._scrollbar:SetValue(min(self._scrollValue, maxScroll))
self._scrollbar.thumb:SetHeight(TSM.UI.Scrollbar.GetLength(totalHeight, self:_GetDimension("HEIGHT")))
local yOffset = -1 * self:_GetPadding("TOP")
for _, child in self:LayoutChildrenIterator() do
local childFrame = child:_GetBaseFrame()
yOffset = yOffset - child:_GetMargin("TOP")
childFrame:SetPoint("TOPLEFT", child:_GetMargin("LEFT") + self:_GetPadding("LEFT"), yOffset)
yOffset = yOffset - childFrame:GetHeight() - child:_GetMargin("BOTTOM")
end
self.__super:Draw()
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function ScrollFrame._OnScrollValueChanged(self, value)
self:_GetBaseFrame():SetVerticalScroll(value)
self._scrollValue = value
end
function ScrollFrame._GetMaxScroll(self)
return max(self._content:GetHeight() - self:_GetDimension("HEIGHT"), 0)
end
function ScrollFrame._GetMinimumDimension(self, dimension)
local styleResult = nil
if dimension == "WIDTH" then
styleResult = self._width
elseif dimension == "HEIGHT" then
styleResult = self._height
else
error("Invalid dimension: "..tostring(dimension))
end
if styleResult then
return styleResult, false
elseif dimension == "HEIGHT" or self:GetNumLayoutChildren() == 0 then
-- regarding the first condition for this if statment, a scrollframe can be any height (including greater than
-- the height of the content if no scrolling is needed), so has no minimum and can always expand
return 0, true
else
-- we're trying to determine the width based on the max width of any of the children
local result = 0
local canExpand = false
for _, child in self:LayoutChildrenIterator() do
local childMin, childCanExpand = child:_GetMinimumDimension(dimension)
childMin = childMin + child:_GetMargin("LEFT") + child:_GetMargin("RIGHT")
canExpand = canExpand or childCanExpand
result = max(result, childMin)
end
result = result + self:_GetPadding("LEFT") + self:_GetPadding("RIGHT")
return result, canExpand
end
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnScrollbarValueChanged(self, value)
value = max(min(value, self:_GetMaxScroll()), 0)
self:_OnScrollValueChanged(value)
end
function private.FrameOnUpdate(self)
if (self:_GetBaseFrame():IsMouseOver() and self:_GetMaxScroll() > 0) or self._scrollbar.dragging then
self._scrollbar:Show()
else
self._scrollbar:Hide()
end
if self._onUpdateHandler then
self:_onUpdateHandler()
end
end
function private.FrameOnMouseWheel(self, direction)
local parentScroll = nil
local parent = self:GetParentElement()
while parent do
if parent:__isa(ScrollFrame) then
parentScroll = parent
break
else
parent = parent:GetParentElement()
end
end
if parentScroll then
local minValue, maxValue = self._scrollbar:GetMinMaxValues()
if direction > 0 then
if self._scrollbar:GetValue() == minValue then
local scrollAmount = min(parentScroll:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
parentScroll._scrollbar:SetValue(parentScroll._scrollbar:GetValue() + -1 * direction * scrollAmount)
else
local scrollAmount = min(self:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
self._scrollbar:SetValue(self._scrollbar:GetValue() + -1 * direction * scrollAmount)
end
else
if self._scrollbar:GetValue() == maxValue then
local scrollAmount = min(parentScroll:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
parentScroll._scrollbar:SetValue(parentScroll._scrollbar:GetValue() + -1 * direction * scrollAmount)
else
local scrollAmount = min(self:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
self._scrollbar:SetValue(self._scrollbar:GetValue() + -1 * direction * scrollAmount)
end
end
else
local scrollAmount = min(self:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
self._scrollbar:SetValue(self._scrollbar:GetValue() + -1 * direction * scrollAmount)
end
end

View File

@@ -0,0 +1,741 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Scrolling Table UI Element Class.
-- A scrolling table contains a scrollable list of rows with a fixed set of columns. It is a subclass of the @{Element}
-- class.
-- @classmod ScrollingTable
local _, TSM = ...
local ObjectPool = TSM.Include("Util.ObjectPool")
local Table = TSM.Include("Util.Table")
local Math = TSM.Include("Util.Math")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Color = TSM.Include("Util.Color")
local Theme = TSM.Include("Util.Theme")
local ScrollingTable = TSM.Include("LibTSMClass").DefineClass("ScrollingTable", TSM.UI.Element, "ABSTRACT")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(ScrollingTable)
TSM.UI.ScrollingTable = ScrollingTable
local private = {
rowPool = ObjectPool.New("TABLE_ROWS", TSM.UI.Util.TableRow, 1),
}
local HEADER_HEIGHT = 22
local HEADER_LINE_HEIGHT = 2
local MORE_COL_WIDTH = 8
local FORCE_DATA_UPDATE = newproxy()
local IGNORE_DATA_UPDATE = newproxy()
local SCROLL_TO_DATA_TOTAL_TIME_S = 0.1
-- ============================================================================
-- Meta Class Methods
-- ============================================================================
function ScrollingTable.__init(self)
local frame = UIElements.CreateFrame(self, "Frame", nil, nil, TSM.IsShadowlands() and "BackdropTemplate" or nil)
self.__super:__init(frame)
frame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
self._lineTop = frame:CreateTexture(nil, "ARTWORK")
self._lineTop:SetPoint("TOPLEFT")
self._lineTop:SetPoint("TOPRIGHT")
self._lineTop:SetHeight(HEADER_LINE_HEIGHT)
self._lineTop:SetColorTexture(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
self._lineBottom = frame:CreateTexture(nil, "ARTWORK")
self._lineBottom:SetPoint("TOPLEFT", 0, -HEADER_HEIGHT - HEADER_LINE_HEIGHT)
self._lineBottom:SetPoint("TOPRIGHT", 0, -HEADER_HEIGHT - HEADER_LINE_HEIGHT)
self._lineBottom:SetHeight(HEADER_LINE_HEIGHT)
self._lineBottom:SetColorTexture(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
self._hScrollFrame = CreateFrame("ScrollFrame", nil, frame)
self._hScrollFrame:SetPoint("TOPLEFT")
self._hScrollFrame:SetPoint("BOTTOMRIGHT")
self._hScrollFrame:EnableMouseWheel(true)
self._hScrollFrame:SetClipsChildren(true)
ScriptWrapper.Set(self._hScrollFrame, "OnUpdate", private.HScrollFrameOnUpdate, self)
ScriptWrapper.Set(self._hScrollFrame, "OnMouseWheel", private.FrameOnMouseWheel, self)
self._hContent = CreateFrame("Frame", nil, self._hScrollFrame)
self._hContent:SetPoint("TOPLEFT")
self._hScrollFrame:SetScrollChild(self._hContent)
self._vScrollFrame = CreateFrame("ScrollFrame", nil, self._hContent)
self._vScrollFrame:SetPoint("TOPLEFT")
self._vScrollFrame:SetPoint("BOTTOMRIGHT")
self._vScrollFrame:EnableMouseWheel(true)
self._vScrollFrame:SetClipsChildren(true)
ScriptWrapper.Set(self._vScrollFrame, "OnUpdate", private.VScrollFrameOnUpdate, self)
ScriptWrapper.Set(self._vScrollFrame, "OnMouseWheel", private.FrameOnMouseWheel, self)
self._content = CreateFrame("Frame", nil, self._vScrollFrame)
self._content:SetPoint("TOPLEFT")
self._vScrollFrame:SetScrollChild(self._content)
self._hScrollbar = TSM.UI.Scrollbar.Create(frame, true)
self._vScrollbar = TSM.UI.Scrollbar.Create(frame)
self._rowHeight = 20
self._backgroundColor = "PRIMARY_BG"
self._rows = {}
self._data = {}
self._hScrollValue = 0
self._vScrollValue = 0
self._onSelectionChangedHandler = nil
self._onRowClickHandler = nil
self._selection = nil
self._selectionDisabled = nil
self._selectionValidator = nil
self._tableInfo = self:_CreateScrollingTableInfo()
self._header = nil
self._dataTranslationFunc = nil
self._contextTable = nil
self._defaultContextTable = nil
self._prevDataOffset = nil
self._lastDataUpdate = nil
self._rowHoverEnabled = true
self._headerHidden = false
self._targetScrollValue = nil
self._totalScrollDistance = nil
self._rightClickToggle = nil
Theme.RegisterChangeCallback(function()
if self:IsVisible() and self._header then
self._header:_LayoutHeaderRow()
end
end)
end
function ScrollingTable.Acquire(self)
self.__super:Acquire()
self._tableInfo:_Acquire(self)
self._hScrollFrame:SetHorizontalScroll(0)
self._hScrollValue = 0
self._vScrollValue = 0
ScriptWrapper.Set(self._vScrollbar, "OnValueChanged", private.OnVScrollbarValueChangedNoDraw, self)
-- don't want to cause this element to be drawn for this initial scrollbar change
self._vScrollbar:SetValue(0)
ScriptWrapper.Set(self._vScrollbar, "OnValueChanged", private.OnVScrollbarValueChanged, self)
ScriptWrapper.Set(self._hScrollbar, "OnValueChanged", private.OnHScrollbarValueChangedNoDraw, self)
-- don't want to cause this element to be drawn for this initial scrollbar change
self._hScrollbar:SetValue(0)
ScriptWrapper.Set(self._hScrollbar, "OnValueChanged", private.OnHScrollbarValueChanged, self)
end
function ScrollingTable.Release(self)
self._rowHeight = 20
self._backgroundColor = "PRIMARY_BG"
self._onSelectionChangedHandler = nil
self._onRowClickHandler = nil
self._selection = nil
self._selectionDisabled = nil
self._selectionValidator = nil
self._dataTranslationFunc = nil
self._contextTable = nil
self._defaultContextTable = nil
self._prevDataOffset = nil
self._lastDataUpdate = nil
if self._header then
self._header:Release()
private.rowPool:Recycle(self._header)
self._header = nil
end
for _, row in ipairs(self._rows) do
row:Release()
private.rowPool:Recycle(row)
end
wipe(self._rows)
self._tableInfo:_Release()
wipe(self._data)
self._headerHidden = false
self._targetScrollValue = nil
self._totalScrollDistance = nil
self.__super:Release()
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
--- Sets the background of the scrolling table.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam number rowHeight The row height
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetRowHeight(self, rowHeight)
self._rowHeight = rowHeight
return self
end
--- Sets the background of the scrolling table.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam boolean hidden Whether or not the header should be hidden
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetHeaderHidden(self, hidden)
self._headerHidden = hidden
return self
end
--- Sets the background of the scrolling table.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam string color The background color as a theme color key
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetBackgroundColor(self, color)
assert(Theme.GetColor(color))
self._backgroundColor = color
return self
end
--- Sets the context table.
-- This table can be used to preserve the table configuration across lifecycles of the scrolling table and even WoW
-- sessions if it's within the settings DB.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam table tbl The context table
-- @tparam table defaultTbl The default table (required fields: `colWidth`, `colHidden`)
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetContextTable(self, tbl, defaultTbl)
assert(type(defaultTbl.colWidth) == "table" and type(defaultTbl.colHidden) == "table")
tbl.colWidth = tbl.colWidth or CopyTable(defaultTbl.colWidth)
tbl.colHidden = tbl.colHidden or CopyTable(defaultTbl.colHidden)
self._contextTable = tbl
self._defaultContextTable = defaultTbl
self:_UpdateColsHidden()
return self
end
--- Sets the context table from a settings object.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam Settings settings The settings object
-- @tparam string key The setting key
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetSettingsContext(self, settings, key)
return self:SetContextTable(settings[key], settings:GetDefaultReadOnly(key))
end
--- Forces an update of the data shown within the table.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam[opt=false] bool redraw Whether or not to redraw the scrolling table
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.UpdateData(self, redraw)
self:_ForceLastDataUpdate()
self:_UpdateData()
if redraw then
self:Draw()
end
return self
end
--- Gets the ScrollingTableInfo object.
-- @tparam ScrollingTable self The scrolling table object
-- @treturn ScrollingTableInfo The scrolling table info object
function ScrollingTable.GetScrollingTableInfo(self)
return self._tableInfo
end
--- Commits the scrolling table info.
-- This should be called once the scrolling table info is completely set (retrieved via @{ScrollingTable.GetScrollingTableInfo}).
-- @tparam ScrollingTable self The scrolling table object
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.CommitTableInfo(self)
self:_UpdateColsHidden()
if self._header then
self._header:Release()
private.rowPool:Recycle(self._header)
self._header = nil
end
return self
end
--- Registers a script handler.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam string script The script to register for (supported scripts: `OnSelectionChanged`, `OnRowClick`)
-- @tparam function handler The script handler which will be called with the scrolling table object followed by any
-- arguments to the script
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetScript(self, script, handler)
if script == "OnSelectionChanged" then
self._onSelectionChangedHandler = handler
elseif script == "OnRowClick" then
self._onRowClickHandler = handler
else
error("Unknown ScrollingTable script: "..tostring(script))
end
return self
end
--- Sets the selected row.
-- @tparam ScrollingTable self The scrolling table object
-- @param selection The selected row or nil to clear the selection
-- @tparam[opt=false] boolean noDraw Don't redraw the rows
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetSelection(self, selection, noDraw)
if selection == self._selection then
self:_JumpToData(selection)
return self
elseif selection and self._selectionValidator and not self:_selectionValidator(selection) then
return self
end
self:_IgnoreLastDataUpdate()
self._selection = selection
self:_JumpToData(selection)
if not noDraw then
for _, row in ipairs(self._rows) do
if not row:IsMouseOver() and row:IsVisible() and not self:_IsSelected(row:GetData()) then
row:SetHighlightState(nil)
elseif row:IsMouseOver() and row:IsVisible() and not self:_IsSelected(row:GetData()) then
row:SetHighlightState("hover")
elseif row:IsMouseOver() and row:IsVisible() and self:_IsSelected(row:GetData()) then
row:SetHighlightState(self._selectionDisabled and "hover" or "selectedHover")
elseif row:IsVisible() and self:_IsSelected(row:GetData()) then
row:SetHighlightState("selected")
end
end
end
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
return self
end
--- Gets the currently selected row.
-- @tparam ScrollingTable self The scrolling table object
-- @return The selected row or nil if there's nothing selected
function ScrollingTable.GetSelection(self)
return self._selection
end
--- Sets a selection validator function.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam function validator A function which gets called with the scrolling table object and a row to validate
-- whether or not it's selectable (returns true if it is, false otherwise)
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetSelectionValidator(self, validator)
self._selectionValidator = validator
return self
end
--- Sets whether or not selection is disabled.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam boolean disabled Whether or not to disable selection
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetSelectionDisabled(self, disabled)
self._selectionDisabled = disabled
return self
end
function ScrollingTable.Draw(self)
self.__super:Draw()
local frame = self:_GetBaseFrame()
local background = Theme.GetColor(self._backgroundColor)
frame:SetBackdropColor(background:GetFractionalRGBA())
if self:_CanResizeCols() then
self:_UpdateColsHidden()
end
if not self._header then
self._header = self:_GetTableRow(true)
self._header:SetBackgroundColor(Theme.GetColor("FRAME_BG"))
self._header:SetHeight(HEADER_HEIGHT)
end
-- update the scrollbar layout
if self._headerHidden then
self._vScrollbar:SetPoint("TOPRIGHT", -Theme.GetScrollbarMargin(), -Theme.GetScrollbarMargin())
else
self._vScrollbar:SetPoint("TOPRIGHT", -Theme.GetScrollbarMargin(), -Theme.GetScrollbarMargin() - HEADER_HEIGHT - HEADER_LINE_HEIGHT * 2)
end
local totalWidth = 0
if self:_CanResizeCols() then
-- add the "more" column
totalWidth = totalWidth + MORE_COL_WIDTH + Theme.GetColSpacing()
for colId, colWidth in pairs(self._contextTable.colWidth) do
if not self._contextTable.colHidden[colId] then
totalWidth = totalWidth + colWidth + Theme.GetColSpacing()
end
end
end
totalWidth = max(totalWidth, self:_GetDimension("WIDTH"))
self._hContent:SetHeight(self._hScrollFrame:GetHeight())
self._hContent:SetWidth(totalWidth)
self._content:SetWidth(self._hContent:GetWidth())
local rowHeight = self._rowHeight
local totalHeight = #self._data * rowHeight
local visibleHeight = self._vScrollFrame:GetHeight()
local visibleWidth = self._hScrollFrame:GetWidth()
local numVisibleRows = min(ceil(visibleHeight / rowHeight), #self._data)
local maxScroll = self:_GetMaxScroll()
local vScrollOffset = min(self._vScrollValue, maxScroll)
local hScrollOffset = min(self._hScrollValue, self:_GetMaxHScroll())
local dataOffset = floor(vScrollOffset / rowHeight)
self._vScrollbar.thumb:SetHeight(TSM.UI.Scrollbar.GetLength(totalHeight, visibleHeight))
self._vScrollbar:SetMinMaxValues(0, maxScroll)
self._vScrollbar:SetValue(vScrollOffset)
self._hScrollbar.thumb:SetWidth(TSM.UI.Scrollbar.GetLength(self._hContent:GetWidth(), visibleWidth))
self._hScrollbar:SetMinMaxValues(0, self:_GetMaxHScroll())
self._hScrollbar:SetValue(hScrollOffset)
self._content:SetHeight(numVisibleRows * rowHeight)
if self._headerHidden then
self._lineTop:Hide()
self._lineBottom:Hide()
self._header:SetHeight(0)
self._header:SetBackgroundColor(Color.GetTransparent())
self._vScrollFrame:SetPoint("TOPLEFT", 0, 0)
else
self._lineTop:SetColorTexture(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
self._lineBottom:SetColorTexture(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
self._lineTop:Show()
self._lineBottom:Show()
self._vScrollFrame:SetPoint("TOPLEFT", 0, -HEADER_HEIGHT - HEADER_LINE_HEIGHT * 2)
self._header:SetBackgroundColor(Theme.GetColor("FRAME_BG"))
self._header:SetHeight(HEADER_HEIGHT)
end
if Math.Round(vScrollOffset + visibleHeight) == totalHeight then
-- we are at the bottom
self._vScrollFrame:SetVerticalScroll(numVisibleRows * rowHeight - visibleHeight)
else
self._vScrollFrame:SetVerticalScroll(0)
end
self._hScrollFrame:SetHorizontalScroll(hScrollOffset)
while #self._rows < numVisibleRows do
local row = self:_GetTableRow(false)
row._frame:SetPoint("TOPLEFT", 0, -rowHeight * #self._rows)
row._frame:SetPoint("TOPRIGHT", 0, -rowHeight * #self._rows)
tinsert(self._rows, row)
end
local scrollDiff = dataOffset - (self._prevDataOffset or dataOffset)
self._prevDataOffset = dataOffset
if scrollDiff ~= 0 then
-- Shuffle the rows around to accomplish the scrolling so that the data only changes
-- for the minimal number of rows, which allows for better optimization
for _ = 1, abs(scrollDiff) do
if scrollDiff > 0 then
tinsert(self._rows, tremove(self._rows, 1))
else
tinsert(self._rows, 1, tremove(self._rows))
end
end
-- fix the points of all the rows
for i, row in ipairs(self._rows) do
row._frame:SetPoint("TOPLEFT", 0, -rowHeight * (i - 1))
row._frame:SetPoint("TOPRIGHT", 0, -rowHeight * (i - 1))
end
end
for i, row in ipairs(self._rows) do
local dataIndex = i + dataOffset
local data = self._data[dataIndex]
if i > numVisibleRows or not data then
row:SetVisible(false)
row:ClearData()
else
row:SetVisible(true)
self:_SetRowData(row, data)
row:SetBackgroundColor(background)
row:SetHeight(rowHeight)
end
end
self._lastDataUpdate = nil
self._header:SetHeaderData()
end
-- ============================================================================
-- ScrollingTable - Private Class Methods
-- ============================================================================
function ScrollingTable._CreateScrollingTableInfo(self)
return TSM.UI.Util.ScrollingTableInfo()
end
function ScrollingTable._GetTableRow(self, isHeader)
local row = private.rowPool:Get()
row:Acquire(self, isHeader)
return row
end
function ScrollingTable._SetRowData(self, row, data)
-- updating the row data is expensive, so only do it if necessary
local dataUpdated = row:GetData() ~= data or not self._lastDataUpdate or self._lastDataUpdate == FORCE_DATA_UPDATE or self._lastDataUpdate == data
local isMouseOver = row:IsMouseOver()
local isSelected = self:_IsSelected(data)
if not isMouseOver and isSelected then
row:SetHighlightState("selected", dataUpdated)
elseif isMouseOver and isSelected then
row:SetHighlightState(self._selectionDisabled and "hover" or "selectedHover", dataUpdated)
elseif isMouseOver and not isSelected then
row:SetHighlightState("hover", dataUpdated)
else
row:SetHighlightState(nil, dataUpdated)
end
if dataUpdated then
row:SetData(data)
end
end
function ScrollingTable._OnScrollValueChanged(self, value, noDraw)
self._vScrollValue = value
if not noDraw then
self:Draw()
end
end
function ScrollingTable._OnHScrollValueChanged(self, value, noDraw)
self._hScrollValue = value
if not noDraw then
self:Draw()
end
end
function ScrollingTable._GetMaxScroll(self)
return max(#self._data * self._rowHeight - self._vScrollFrame:GetHeight(), 0)
end
function ScrollingTable._GetMaxHScroll(self)
return max(self._hContent:GetWidth() - self._hScrollFrame:GetWidth(), 0)
end
function ScrollingTable._UpdateData(self)
error("Must be implemented by the child class")
end
function ScrollingTable._ToggleSort(self, id)
error("Must be implemented by the child class")
end
function ScrollingTable._IsSelected(self, data)
return data == self._selection
end
function ScrollingTable._HandleRowClick(self, data, mouseButton)
if self._onRowClickHandler then
self:_onRowClickHandler(data, mouseButton)
end
end
function ScrollingTable._GetColWidth(self, id)
return self._contextTable.colWidth[id]
end
function ScrollingTable._ResetColWidth(self, id)
local defaultWidth = self._defaultContextTable.colWidth[id]
local currentWidth = self._contextTable.colWidth[id]
assert(currentWidth and defaultWidth)
self._contextTable.colWidth[id] = defaultWidth
self._header:_LayoutHeaderRow()
for _, row in ipairs(self._rows) do
row:_LayoutDataRow()
end
self:Draw()
end
function ScrollingTable._SetColWidth(self, id, width, redraw)
assert(not self._contextTable.colWidthLocked)
local prevWidth = self._contextTable.colWidth[id]
assert(prevWidth)
if width == prevWidth and not redraw then
return
end
self._contextTable.colWidth[id] = width
for _, row in ipairs(self._rows) do
row:_LayoutDataRow()
end
if redraw then
self:Draw()
end
end
function ScrollingTable._IsColWidthLocked(self)
return self._contextTable.colWidthLocked
end
function ScrollingTable._ToogleColWidthLocked(self)
self._contextTable.colWidthLocked = not self._contextTable.colWidthLocked or nil
self._header:_LayoutHeaderRow()
self:Draw()
end
function ScrollingTable._CanResizeCols(self)
return self._contextTable and true or false
end
function ScrollingTable._ToggleColHide(self, id)
if not self._contextTable then
return
end
self._contextTable.colHidden[id] = not self._contextTable.colHidden[id] or nil
self:_UpdateColsHidden()
self._header:_LayoutHeaderRow()
for _, row in ipairs(self._rows) do
row:_LayoutDataRow()
end
self:Draw()
end
function ScrollingTable._ResetContext(self)
assert(self._contextTable)
if self._defaultContextTable.colWidth then
wipe(self._contextTable.colWidth)
for col, width in pairs(self._defaultContextTable.colWidth) do
self._contextTable.colWidth[col] = width
end
end
if self._defaultContextTable.colHidden then
wipe(self._contextTable.colHidden)
for col, hidden in pairs(self._defaultContextTable.colHidden) do
self._contextTable.colHidden[col] = hidden
end
self:_UpdateColsHidden()
end
self._header:_LayoutHeaderRow()
for _, row in ipairs(self._rows) do
row:_LayoutDataRow()
end
self:Draw()
end
function ScrollingTable._UpdateColsHidden(self)
for _, col in self:GetScrollingTableInfo():_ColIterator() do
local colId = col:_GetId()
if col:_CanHide() then
col:_SetHidden(self._contextTable and self._contextTable.colHidden[colId] and true or false)
elseif self._contextTable then
self._contextTable.colHidden[colId] = nil
end
end
end
function ScrollingTable._SetLastDataUpdate(self, value)
self._lastDataUpdate = value
end
function ScrollingTable._IgnoreLastDataUpdate(self)
self._lastDataUpdate = IGNORE_DATA_UPDATE
end
function ScrollingTable._ForceLastDataUpdate(self)
self._lastDataUpdate = FORCE_DATA_UPDATE
end
function ScrollingTable._ScrollToData(self, data)
local rowHeight = self._rowHeight
local visibleHeight = self._vScrollFrame:GetHeight()
local currentOffset = self._vScrollbar:GetValue()
local dataIndex = Table.KeyByValue(self._data, data)
-- if we are going to scroll up/down, we want to scroll such that the top of the passed row is in the visible area
-- by at least 1 row height
local scrollUpOffset = max(rowHeight * (dataIndex - 1) - rowHeight, 0)
local scrollDownOffset = min(rowHeight * dataIndex + rowHeight - visibleHeight, self:_GetMaxScroll())
if scrollUpOffset < currentOffset and scrollDownOffset > currentOffset then
-- it's impossible to scroll to the right place, so do nothing
elseif scrollUpOffset < currentOffset then
-- we need to scroll up
self._targetScrollValue = scrollUpOffset
self._totalScrollDistance = currentOffset - scrollUpOffset
elseif scrollDownOffset > currentOffset then
-- we need to scroll down
self._targetScrollValue = scrollDownOffset
self._totalScrollDistance = scrollDownOffset - currentOffset
else
-- the data is already in the visible area, so do nothing
end
end
function ScrollingTable._JumpToData(self, data)
if not data then
return
end
local index = Table.KeyByValue(self._data, data)
assert(index)
-- set the scroll so that the selection is visible if necessary
local rowHeight = self._rowHeight
local firstVisibleIndex = ceil(self._vScrollValue / rowHeight) + 1
local lastVisibleIndex = floor((self._vScrollValue + self:_GetDimension("HEIGHT")) / rowHeight)
if lastVisibleIndex > firstVisibleIndex and (index < firstVisibleIndex or index > lastVisibleIndex) then
self:_OnScrollValueChanged(min((index - 1) * rowHeight, self:_GetMaxScroll()))
end
end
-- ============================================================================
-- ScrollingTable - Local Script Handlers
-- ============================================================================
function private.OnHScrollbarValueChanged(self, value)
value = max(min(value, self:_GetMaxHScroll()), 0)
self:_OnHScrollValueChanged(value)
end
function private.OnVScrollbarValueChanged(self, value)
value = max(min(value, self:_GetMaxScroll()), 0)
self:_OnScrollValueChanged(value)
end
function private.OnHScrollbarValueChangedNoDraw(self, value)
value = max(min(value, self:_GetMaxHScroll()), 0)
self:_OnHScrollValueChanged(value, true)
end
function private.OnVScrollbarValueChangedNoDraw(self, value)
value = max(min(value, self:_GetMaxScroll()), 0)
self:_OnScrollValueChanged(value, true)
end
function private.HScrollFrameOnUpdate(self)
if (self._hScrollFrame:IsMouseOver() and self:_GetMaxHScroll() > 1) or self._hScrollbar.dragging then
self._hScrollbar:Show()
else
self._hScrollbar:Hide()
end
end
function private.VScrollFrameOnUpdate(self, elapsed)
elapsed = min(elapsed, 0.01)
if self._targetScrollValue then
local scrollValue = self._vScrollbar:GetValue()
local direction = scrollValue < self._targetScrollValue and 1 or -1
local newScrollValue = scrollValue + direction * self._totalScrollDistance * elapsed / SCROLL_TO_DATA_TOTAL_TIME_S
self._vScrollbar:SetValue(newScrollValue)
if direction * newScrollValue >= direction * self._targetScrollValue or newScrollValue <= 0 or newScrollValue >= self:_GetMaxScroll() then
-- we are done scrolling
self._targetScrollValue = nil
self._totalScrollDistance = nil
end
end
local rOffset = max(self._hContent:GetWidth() - self._hScrollFrame:GetWidth() - self._hScrollbar:GetValue(), 0)
if (self._vScrollFrame:IsMouseOver(0, 0, 0, -rOffset) and self:_GetMaxScroll() > 1) or self._vScrollbar.dragging then
self._vScrollbar:Show()
else
self._vScrollbar:Hide()
end
end
function private.FrameOnMouseWheel(self, direction)
local scrollAmount = -direction * Theme.GetMouseWheelScrollAmount()
if IsShiftKeyDown() and self._hScrollbar:IsVisible() then
-- scroll horizontally
self._hScrollbar:SetValue(self._hScrollbar:GetValue() + scrollAmount)
else
self._vScrollbar:SetValue(self._vScrollbar:GetValue() + scrollAmount)
end
end

View File

@@ -0,0 +1,165 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Search List UI Element Class.
-- A search list contains a list of recent or favorite searches. It is a subclass of the @{ScrollingTable} class.
-- @classmod SearchList
local _, TSM = ...
local SearchList = TSM.Include("LibTSMClass").DefineClass("SearchList", TSM.UI.ScrollingTable)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(SearchList)
TSM.UI.SearchList = SearchList
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function SearchList.__init(self)
self.__super:__init()
self._onRowClickHandler = nil
self._onFavoriteChangedHandler = nil
self._onEditClickHandler = nil
self._onDeleteHandler = nil
self._query = nil
self._editBtnHidden = false
end
function SearchList.Acquire(self)
self._headerHidden = true
self.__super:Acquire()
self:GetScrollingTableInfo()
:NewColumn("name")
:SetFont("BODY_BODY3_MEDIUM")
:SetJustifyH("LEFT")
:SetTextFunction(private.GetNameText)
:SetActionIconInfo(3, 18, private.GetActionIcon, true)
:SetActionIconClickHandler(private.OnActionIconClick)
:DisableHiding()
:Commit()
:Commit()
end
function SearchList.Release(self)
self._onRowClickHandler = nil
self._onFavoriteChangedHandler = nil
self._onEditClickHandler = nil
self._onDeleteHandler = nil
if self._query then
self._query:Release()
self._query = nil
end
self._editBtnHidden = false
self.__super:Release()
end
--- Sets whether or not the edit button is hidden.
-- @tparam SearchList self The search list object
-- @tparam boolean hidden Whether or not the edit button is hidden
-- @treturn SearchList The search list object
function SearchList.SetEditButtonHidden(self, hidden)
self._editBtnHidden = hidden
return self
end
--- Sets the @{DatabaseQuery} source for this list.
-- This query is used to populate the entries in the search list.
-- @tparam SearchList self The search list object
-- @tparam DatabaseQuery query The query object
-- @tparam[opt=false] bool redraw Whether or not to redraw the search list
-- @treturn SearchList The search list object
function SearchList.SetQuery(self, query, redraw)
if self._query then
self._query:Release()
end
self._query = query
self._query:SetUpdateCallback(private.QueryUpdateCallback, self)
self:UpdateData(redraw)
return self
end
--- Registers a script handler.
-- @tparam SearchList self The search list object
-- @tparam string script The script to register for (supported scripts: `OnRowClick`, `OnFavoriteChanged`,
-- `OnEditClick`, `OnDelete`)
-- @tparam function handler The script handler which will be called with the search list object followed by any
-- arguments to the script
-- @treturn SearchList The search list object
function SearchList.SetScript(self, script, handler)
if script == "OnRowClick" then
self._onRowClickHandler = handler
elseif script == "OnFavoriteChanged" then
self._onFavoriteChangedHandler = handler
elseif script == "OnEditClick" then
self._onEditClickHandler = handler
elseif script == "OnDelete" then
self._onDeleteHandler = handler
else
error("Unknown SearchList script: "..tostring(script))
end
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function SearchList._UpdateData(self)
wipe(self._data)
for _, row in self._query:Iterator() do
tinsert(self._data, row)
end
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.GetNameText(self, data)
return data:GetField("name")
end
function private.GetActionIcon(self, data, iconIndex, isMouseOver)
if iconIndex == 1 then
return true, data:GetField("isFavorite") and "iconPack.18x18/Star/Filled" or "iconPack.18x18/Star/Unfilled"
elseif iconIndex == 2 then
if self._editBtnHidden then
return false, nil
end
return true, "iconPack.18x18/Edit"
elseif iconIndex == 3 then
return true, "iconPack.18x18/Delete"
else
error("Invalid iconIndex: "..tostring(iconIndex))
end
end
function private.OnActionIconClick(self, data, iconIndex)
if iconIndex == 1 then
-- favorite
self:_onFavoriteChangedHandler(data, not data:GetField("isFavorite"))
elseif iconIndex == 2 then
-- edit
assert(not self._editBtnHidden)
self:_onEditClickHandler(data)
elseif iconIndex == 3 then
-- delete
self:_onDeleteHandler(data)
else
error("Invalid iconIndex: "..tostring(iconIndex))
end
end
function private.QueryUpdateCallback(_, _, self)
self:UpdateData(true)
end

View File

@@ -0,0 +1,61 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- SecureMacroActionButton UI Element Class.
-- A secure macro action button builds on top of WoW's `SecureActionButtonTemplate` to allow executing scripts which
-- addon buttons would otherwise be forbidden from running. It is a subclass of the @{ActionButton} class.
-- @classmod SecureMacroActionButton
local _, TSM = ...
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local SecureMacroActionButton = TSM.Include("LibTSMClass").DefineClass("SecureMacroActionButton", TSM.UI.ActionButton)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(SecureMacroActionButton)
TSM.UI.SecureMacroActionButton = SecureMacroActionButton
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function SecureMacroActionButton.__init(self, name)
self.__super:__init(name, true)
local frame = self:_GetBaseFrame()
frame:SetAttribute("type1", "macro")
frame:SetAttribute("macrotext1", "")
end
function SecureMacroActionButton.Release(self)
local frame = self:_GetBaseFrame()
ScriptWrapper.Clear(frame, "PreClick")
frame:SetAttribute("macrotext1", "")
self.__super:Release()
end
--- Registers a script handler.
-- @tparam SecureMacroActionButton self The secure macro action button object
-- @tparam string script The script to register for (supported scripts: `PreClick`)
-- @tparam function handler The script handler which will be called with the secure macro action button object followed
-- by any arguments to the script
-- @treturn SecureMacroActionButton The secure macro action button object
function SecureMacroActionButton.SetScript(self, script, handler)
if script == "PreClick" or script == "PostClick" then
self.__super.__super:SetScript(script, handler)
else
error("Unknown SecureActionButton script: "..tostring(script))
end
return self
end
--- Sets the macro text which clicking the button executes.
-- @tparam SecureMacroActionButton self The secure macro action button object
-- @tparam string text THe macro text
-- @treturn SecureMacroActionButton The secure macro action button object
function SecureMacroActionButton.SetMacroText(self, text)
self:_GetBaseFrame():SetAttribute("macrotext1", text)
return self
end

View File

@@ -0,0 +1,121 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Selection Dropdown UI Element Class.
-- A dropdown element allows the user to select from a dialog list. It is a subclass of the @{BaseDropdown} class.
-- @classmod SelectionDropdown
local _, TSM = ...
local Table = TSM.Include("Util.Table")
local SelectionDropdown = TSM.Include("LibTSMClass").DefineClass("SelectionDropdown", TSM.UI.BaseDropdown)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(SelectionDropdown)
TSM.UI.SelectionDropdown = SelectionDropdown
-- ============================================================================
-- Meta Class Methods
-- ============================================================================
function SelectionDropdown.__init(self)
self.__super:__init()
self._selectedItem = nil
self._settingTable = nil
self._settingKey = nil
end
function SelectionDropdown.Release(self)
self._selectedItem = nil
self._settingTable = nil
self._settingKey = nil
self.__super:Release()
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
--- Set the currently selected item.
-- @tparam SelectionDropdown self The dropdown object
-- @tparam ?string item The selected item or nil if nothing should be selected
-- @tparam[opt=false] boolean silent Don't call the OnSelectionChanged callback
-- @treturn SelectionDropdown The dropdown object
function SelectionDropdown.SetSelectedItem(self, item, silent)
self._selectedItem = item
if self._settingTable then
self._settingTable[self._settingKey] = self._itemKeyLookup[item]
end
if not silent and self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
return self
end
--- Set the currently selected item by key.
-- @tparam SelectionDropdown self The dropdown object
-- @tparam ?string itemKey The key for the selected item or nil if nothing should be selected
-- @tparam[opt=false] boolean silent Don't call the OnSelectionChanged callback
-- @treturn SelectionDropdown The dropdown object
function SelectionDropdown.SetSelectedItemByKey(self, itemKey, silent)
local item = itemKey and Table.GetDistinctKey(self._itemKeyLookup, itemKey) or nil
self:SetSelectedItem(item, silent)
return self
end
--- Get the currently selected item.
-- @tparam SelectionDropdown self The dropdown object
-- @treturn ?string The selected item
function SelectionDropdown.GetSelectedItem(self)
return self._selectedItem
end
--- Get the currently selected item.
-- @tparam SelectionDropdown self The dropdown object
-- @treturn ?string The selected item key
function SelectionDropdown.GetSelectedItemKey(self)
return self._selectedItem and self._itemKeyLookup[self._selectedItem] or nil
end
--- Sets the setting info.
-- This method is used to have the value of the dropdown automatically correspond with the value of a field in a table.
-- This is useful for dropdowns which are tied directly to settings.
-- @tparam SelectionDropdown self The dropdown object
-- @tparam table tbl The table which the field to set belongs to
-- @tparam string key The key into the table to be set based on the dropdown state
-- @treturn SelectionDropdown The dropdown object
function SelectionDropdown.SetSettingInfo(self, tbl, key)
self._settingTable = tbl
self._settingKey = key
if tbl then
self:SetSelectedItemByKey(tbl[key])
end
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function SelectionDropdown._AddDialogChildren(self, frame)
frame:AddChild(UIElements.New("DropdownList", "list")
:SetMultiselect(false)
:SetItems(self._items, self._selectedItem)
)
end
function SelectionDropdown._GetCurrentSelectionString(self)
return self._selectedItem or self._hintText
end
function SelectionDropdown._OnListSelectionChanged(self, _, selection)
self:SetOpen(false)
self:SetSelectedItem(selection)
end

View File

@@ -0,0 +1,117 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- SelectionGroupTree UI Element Class.
-- A selection group tree allows for selecting a single group within the tree. It is a subclass of the @{GroupTree} class.
-- @classmod SelectionGroupTree
local _, TSM = ...
local Table = TSM.Include("Util.Table")
local UIElements = TSM.Include("UI.UIElements")
local SelectionGroupTree = TSM.Include("LibTSMClass").DefineClass("SelectionGroupTree", TSM.UI.GroupTree)
UIElements.Register(SelectionGroupTree)
TSM.UI.SelectionGroupTree = SelectionGroupTree
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function SelectionGroupTree.__init(self)
self.__super:__init()
self._selectedGroup = TSM.CONST.ROOT_GROUP_PATH
self._selectedGroupChangedHandler = nil
end
function SelectionGroupTree.Release(self)
self._selectedGroupChangedHandler = nil
self._selectedGroup = TSM.CONST.ROOT_GROUP_PATH
self.__super:Release()
end
--- Sets the selected group path.
-- @tparam SelectionGroupTree self The selection group tree object
-- @tparam string groupPath The group path string to select
-- @treturn SelectionGroupTree The application group tree object
function SelectionGroupTree.SetSelection(self, groupPath)
assert(groupPath)
self._selectedGroup = groupPath
wipe(self._contextTable.selected)
self._contextTable.selected[groupPath] = true
local index = Table.KeyByValue(self._data, groupPath)
assert(index)
-- set the scroll so that the selection is visible if necessary
local rowHeight = self._rowHeight
local firstVisibleIndex = ceil(self._vScrollValue / rowHeight) + 1
local lastVisibleIndex = floor((self._vScrollValue + self:_GetDimension("HEIGHT")) / rowHeight)
if lastVisibleIndex > firstVisibleIndex and (index < firstVisibleIndex or index > lastVisibleIndex) then
self:_OnScrollValueChanged(min((index - 1) * rowHeight, self:_GetMaxScroll()))
end
return self
end
--- Gets the selected group path.
-- @tparam SelectionGroupTree self The selection group tree object
-- @treturn string The currently selected group path string
function SelectionGroupTree.GetSelection(self)
return self._selectedGroup
end
--- Sets the context table.
-- This table can be used to preserve selection state across lifecycles of the application group tree and even WoW
-- sessions if it's within the settings DB.
-- @see GroupTree.SetContextTable
-- @tparam SelectionGroupTree self The application group tree object
-- @tparam table tbl The context table
-- @tparam table defaultTbl The default table (required fields: `selected`, `collapsed`)
-- @treturn SelectionGroupTree The application group tree object
function SelectionGroupTree.SetContextTable(self, tbl, defaultTbl)
assert(type(defaultTbl.selected) == "table")
tbl.selected = tbl.selected or CopyTable(defaultTbl.selected)
self.__super:SetContextTable(tbl, defaultTbl)
return self
end
--- Registers a script handler.
-- @tparam SelectionGroupTree self The selection group tree object
-- @tparam string script The script to register for (supported scripts: `OnGroupSelectionChanged`)
-- @tparam function handler The script handler which will be called with the selection group tree object followed by any
-- arguments to the script
-- @treturn SelectionGroupTree The selection group tree object
function SelectionGroupTree.SetScript(self, script, handler)
if script == "OnGroupSelectionChanged" then
self._selectedGroupChangedHandler = handler
else
error("Unknown SelectionGroupTree script: "..tostring(script))
end
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function SelectionGroupTree._HandleRowClick(self, data, mouseButton)
if mouseButton == "RightButton" then
self.__super:_HandleRowClick(data, mouseButton)
return
end
self._selectedGroup = data
wipe(self._contextTable.selected)
self._contextTable.selected[data] = true
self:Draw()
if self._selectedGroupChangedHandler then
self:_selectedGroupChangedHandler(data)
end
end
function SelectionGroupTree._IsSelected(self, data)
return data == self._selectedGroup
end

View File

@@ -0,0 +1,101 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- SelectionList UI Element Class.
-- A selection list is a scrollable list of entries which allows selecting a single one. It is a subclass of the
-- @{ScrollingTable} class.
-- @classmod SelectionList
local _, TSM = ...
local Theme = TSM.Include("Util.Theme")
local SelectionList = TSM.Include("LibTSMClass").DefineClass("SelectionList", TSM.UI.ScrollingTable)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(SelectionList)
TSM.UI.SelectionList = SelectionList
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function SelectionList.__init(self)
self.__super:__init()
self._selectedEntry = nil
self._onEntrySelectedHandler = nil
end
function SelectionList.Acquire(self)
self._headerHidden = true
self.__super:Acquire()
self:SetSelectionDisabled(true)
self:GetScrollingTableInfo()
:NewColumn("text")
:SetFont("BODY_BODY2")
:SetJustifyH("LEFT")
:SetTextFunction(private.GetText)
:DisableHiding()
:Commit()
:Commit()
end
function SelectionList.Release(self)
self._selectedEntry = nil
self._onEntrySelectedHandler = nil
self.__super:Release()
end
--- Sets the entries.
-- @tparam SelectionList self The selection list object
-- @tparam table entries A list of entries
-- @tparam string selectedEntry The selected entry
-- @treturn SelectionList The selection list object
function SelectionList.SetEntries(self, entries, selectedEntry)
wipe(self._data)
for _, entry in ipairs(entries) do
tinsert(self._data, entry)
end
self._selectedEntry = selectedEntry
return self
end
--- Registers a script handler.
-- @tparam SelectionList self The selection list object
-- @tparam string script The script to register for (supported scripts: `OnEntrySelected`)
-- @tparam function handler The script handler which will be called with the selection list object followed by any
-- arguments to the script
-- @treturn SelectionList The selection list object
function SelectionList.SetScript(self, script, handler)
if script == "OnEntrySelected" then
self._onEntrySelectedHandler = handler
else
error("Unknown SelectionList script: "..tostring(script))
end
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function SelectionList._HandleRowClick(self, data)
if self._onEntrySelectedHandler then
self:_onEntrySelectedHandler(data)
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetText(self, data)
return Theme.GetColor(data == self._selectedEntry and "INDICATOR" or "TEXT"):ColorText(data)
end

View File

@@ -0,0 +1,250 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- SelectionScrollingTable UI Element Class.
-- A selection scrolling table is a scrolling table which allows for selecting and deselecting individual rows. It is a
-- subclass of the @{QueryScrollingTable} class.
-- @classmod SelectionScrollingTable
local _, TSM = ...
local Table = TSM.Include("Util.Table")
local TempTable = TSM.Include("Util.TempTable")
local UIElements = TSM.Include("UI.UIElements")
local SelectionScrollingTable = TSM.Include("LibTSMClass").DefineClass("SelectionScrollingTable", TSM.UI.QueryScrollingTable)
UIElements.Register(SelectionScrollingTable)
TSM.UI.SelectionScrollingTable = SelectionScrollingTable
local private = {
querySelectionScrollingTableLookup = {},
sortValuesTemp = {},
tempContextTable = {},
}
local TEMP_CONTEXT_TABLE_DEFAULT = {
colWidth = {
selected = 16,
},
colHidden = {},
}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function SelectionScrollingTable.__init(self)
self.__super:__init()
self._selectedData = {}
self._selectionEnabledFunc = nil
self._rightClickToggle = true
end
function SelectionScrollingTable.Acquire(self)
self.__super:Acquire()
-- temporarily set a context table so we can create the table columns (should be overridden later)
wipe(private.tempContextTable)
self.__super:SetContextTable(private.tempContextTable, TEMP_CONTEXT_TABLE_DEFAULT)
self:GetScrollingTableInfo()
:NewColumn("selected", true)
:SetTitleIcon("iconPack.14x14/Checkmark/Default")
:SetWidth(14)
:SetIconSize(12)
:SetFont("ITEM_BODY3")
:SetJustifyH("CENTER")
:SetIconInfo(nil, private.GetSelectedIcon)
:DisableHiding()
:Commit()
:Commit()
end
function SelectionScrollingTable.Release(self)
private.querySelectionScrollingTableLookup[self._query] = nil
wipe(self._selectedData)
self._selectionEnabledFunc = nil
self.__super:Release()
end
--- Sets the @{DatabaseQuery} source for this table.
-- This query is used to populate the entries in the selection scrolling table.
-- @tparam SelectionScrollingTable self The selection scrolling table object
-- @tparam DatabaseQuery query The query object
-- @tparam[opt=false] bool redraw Whether or not to redraw the selection scrolling table
-- @treturn SelectionScrollingTable The selection scrolling table object
function SelectionScrollingTable.SetQuery(self, query, redraw)
if self._query then
private.querySelectionScrollingTableLookup[self._query] = nil
end
private.querySelectionScrollingTableLookup[query] = self
self.__super:SetQuery(query, redraw)
return self
end
--- Selects all items.
-- @tparam SelectionScrollingTable self The selection scrolling table object
function SelectionScrollingTable.SelectAll(self)
for _, uuid in ipairs(self._data) do
self._selectedData[uuid] = true
end
self:_UpdateData()
self:Draw()
if self._onSelectionChangedHandler then
self._onSelectionChangedHandler(self)
end
end
--- Clear the selection.
-- @tparam SelectionScrollingTable self The selection scrolling table object
function SelectionScrollingTable.ClearSelection(self)
wipe(self._selectedData)
self:_UpdateData()
self:Draw()
if self._onSelectionChangedHandler then
self._onSelectionChangedHandler(self)
end
end
--- Sets a selection enabled function.
-- @tparam SelectionScrollingTable self The selection scrolling table object
-- @tparam function func A funciton which gets called with data to determine if it's selectable or not
-- @treturn SelectionScrollingTable The selection scrolling table object
function SelectionScrollingTable.SetIsSelectionEnabledFunc(self, func)
self._selectionEnabledFunc = func
return self
end
--- Toggles the selection of a record.
-- @tparam SelectionScrollingTable self The selection scrolling table object
-- @tparam ?table data The record to toggle the selection of
-- @treturn SelectionScrollingTable The selection scrolling table object
function SelectionScrollingTable.SetSelection(self, data)
if data and self._selectionValidator and not self:_selectionValidator(self._query:GetResultRowByUUID(data)) then
assert(not self._selectedData[data])
return self
end
self._selectedData[data] = not self._selectedData[data] or nil
for _, row in ipairs(self._rows) do
if row:GetData() == data then
self:_SetRowData(row, data)
break
end
end
if self._sortCol == "selected" then
self:_UpdateData()
self:Draw()
end
if self._onSelectionChangedHandler then
self._onSelectionChangedHandler(self)
end
return self
end
--- Gets whether or not all of the items are currently selected.
-- @tparam SelectionScrollingTable self The selection scrolling table object
-- @treturn boolean Whether or not all of the selection is selected
function SelectionScrollingTable.IsAllSelected(self)
for _, uuid in ipairs(self._data) do
if not self._selectedData[uuid] then
return false
end
end
return true
end
--- Gets whether or not the selection is currently cleared.
-- @tparam SelectionScrollingTable self The selection scrolling table object
-- @treturn boolean Whether or not the selection is cleared
function SelectionScrollingTable.IsSelectionCleared(self)
return not next(self._selectedData)
end
--- Gets the current selection table.
-- @tparam SelectionScrollingTable self The selection scrolling table object
-- @treturn table A table where the key is the data and the value is whether or not it's selected (only selected entries
-- are in the table)
function SelectionScrollingTable.SelectionIterator(self)
return private.SelectionIteratorHelper, self
end
--- Sets the context table.
-- This table can be used to preserve the table configuration across lifecycles of the scrolling table and even WoW
-- sessions if it's within the settings DB.
-- @tparam SelectionScrollingTable self The selection scrolling table object
-- @tparam table tbl The context table
-- @tparam table defaultTbl The default table (required fields: `colWidth`)
-- @treturn SelectionScrollingTable The selection scrolling table object
function SelectionScrollingTable.SetContextTable(self, tbl, defaultTbl)
assert(defaultTbl.colWidth.selected)
self.__super:SetContextTable(tbl, defaultTbl)
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function SelectionScrollingTable._IsSelected(self, data)
return self._selectedData[data] == 1
end
function SelectionScrollingTable._UpdateData(self)
self.__super:_UpdateData()
-- clear any old selection context
local hasData = TempTable.Acquire()
for _, data in ipairs(self._data) do
hasData[data] = true
end
for data in pairs(self._selectedData) do
if not hasData[data] then
self._selectedData[data] = nil
end
end
TempTable.Release(hasData)
if self._sortCol == "selected" then
local selectedValue = self._sortAscending and -1 or 1
for _, uuid in ipairs(self._data) do
private.sortValuesTemp[uuid] = self._selectedData[uuid] and selectedValue or 0
end
Table.SortWithValueLookup(self._data, private.sortValuesTemp)
wipe(private.sortValuesTemp)
end
end
function SelectionScrollingTable._ToggleSort(self, id)
if id ~= "selected" then
return self.__super:_ToggleSort(id)
end
if id == self._sortCol then
self._sortAscending = not self._sortAscending
else
self._sortCol = id
self._sortAscending = true
end
self:_UpdateData()
self:Draw()
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.SelectionIteratorHelper(self, uuid)
uuid = next(self._selectedData, uuid)
if not uuid then
return
end
return uuid, self._query:GetResultRowByUUID(uuid)
end
function private.GetSelectedIcon(row)
local self = private.querySelectionScrollingTableLookup[row:GetQuery()]
return self._selectedData[row:GetUUID()] and "iconPack.14x14/Checkmark/Default" or 0
end

View File

@@ -0,0 +1,105 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- SimpleTabGroup UI Element Class.
-- A simple table group uses text to denote tabs with the selected one colored differently. It is a subclass of the
-- @{ViewContainer} class.
-- @classmod SimpleTabGroup
local _, TSM = ...
local SimpleTabGroup = TSM.Include("LibTSMClass").DefineClass("SimpleTabGroup", TSM.UI.ViewContainer)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(SimpleTabGroup)
TSM.UI.SimpleTabGroup = SimpleTabGroup
local private = {}
local BUTTON_HEIGHT = 24
local BUTTON_PADDING_BOTTOM = 2
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function SimpleTabGroup.__init(self)
self.__super:__init()
self._buttons = {}
end
function SimpleTabGroup.Acquire(self)
self.__super.__super:AddChildNoLayout(UIElements.New("Frame", "buttons")
:SetLayout("HORIZONTAL")
:AddAnchor("TOPLEFT")
:AddAnchor("TOPRIGHT")
)
self.__super:Acquire()
end
function SimpleTabGroup.Release(self)
wipe(self._buttons)
self.__super:Release()
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function SimpleTabGroup._GetContentPadding(self, side)
if side == "TOP" then
return BUTTON_HEIGHT + BUTTON_PADDING_BOTTOM
end
return self.__super:_GetContentPadding(side)
end
function SimpleTabGroup.Draw(self)
self.__super.__super.__super:Draw()
local selectedPath = self:GetPath()
local buttons = self:GetElement("buttons")
buttons:SetHeight(BUTTON_HEIGHT + BUTTON_PADDING_BOTTOM)
buttons:ReleaseAllChildren()
for i, buttonPath in ipairs(self._pathsList) do
local isSelected = buttonPath == selectedPath
buttons:AddChild(UIElements.New("Button", self._id.."_Tab"..i)
:SetWidth("AUTO")
:SetMargin(8, 8, 0, BUTTON_PADDING_BOTTOM)
:SetJustifyH("LEFT")
:SetFont("BODY_BODY1_BOLD")
:SetTextColor(isSelected and "INDICATOR" or "TEXT_ALT")
:SetContext(self)
:SetText(buttonPath)
:SetScript("OnEnter", not isSelected and private.OnButtonEnter)
:SetScript("OnLeave", not isSelected and private.OnButtonLeave)
:SetScript("OnClick", private.OnButtonClicked)
)
end
self.__super:Draw()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnButtonEnter(button)
button:SetTextColor("TEXT")
:Draw()
end
function private.OnButtonLeave(button)
button:SetTextColor("TEXT_ALT")
:Draw()
end
function private.OnButtonClicked(button)
local self = button:GetContext()
local path = button:GetText()
self:SetPath(path, true)
end

266
Core/UI/Elements/Slider.lua Normal file
View File

@@ -0,0 +1,266 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Slider UI Element Class.
-- A slider allows for selecting a numerical range. It is a subclass of the @{Element} class.
-- @classmod Slider
local _, TSM = ...
local Math = TSM.Include("Util.Math")
local NineSlice = TSM.Include("Util.NineSlice")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Theme = TSM.Include("Util.Theme")
local Slider = TSM.Include("LibTSMClass").DefineClass("Slider", TSM.UI.Element)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Slider)
TSM.UI.Slider = Slider
local private = {}
local THUMB_WIDTH = 8
local INPUT_WIDTH = 50
local INPUT_AREA_SPACE = 128
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Slider.__init(self)
local frame = UIElements.CreateFrame(self, "Frame")
frame:EnableMouse(true)
ScriptWrapper.Set(frame, "OnMouseDown", private.FrameOnMouseDown, self)
ScriptWrapper.Set(frame, "OnMouseUp", private.FrameOnMouseUp, self)
ScriptWrapper.Set(frame, "OnUpdate", private.FrameOnUpdate, self)
self.__super:__init(frame)
-- create the textures
frame.barTexture = frame:CreateTexture(nil, "BACKGROUND", nil, 1)
frame.activeBarTexture = frame:CreateTexture(nil, "BACKGROUND", nil, 2)
frame.thumbTextureLeft = frame:CreateTexture(nil, "ARTWORK")
frame.thumbTextureRight = frame:CreateTexture(nil, "ARTWORK")
frame.inputLeft = CreateFrame("EditBox", nil, frame, nil)
frame.inputLeft:SetJustifyH("CENTER")
frame.inputLeft:SetWidth(INPUT_WIDTH)
frame.inputLeft:SetHeight(24)
frame.inputLeft:SetAutoFocus(false)
frame.inputLeft:SetNumeric(true)
ScriptWrapper.Set(frame.inputLeft, "OnEscapePressed", private.InputOnEscapePressed)
ScriptWrapper.Set(frame.inputLeft, "OnEnterPressed", private.LeftInputOnEnterPressed, self)
frame.dash = UIElements.CreateFontString(self, frame)
frame.dash:SetJustifyH("CENTER")
frame.dash:SetJustifyV("MIDDLE")
frame.dash:SetWidth(12)
frame.inputRight = CreateFrame("EditBox", nil, frame, nil)
frame.inputRight:SetJustifyH("CENTER")
frame.inputRight:SetWidth(INPUT_WIDTH)
frame.inputRight:SetHeight(24)
frame.inputRight:SetNumeric(true)
frame.inputRight:SetAutoFocus(false)
ScriptWrapper.Set(frame.inputRight, "OnEscapePressed", private.InputOnEscapePressed)
ScriptWrapper.Set(frame.inputRight, "OnEnterPressed", private.RightInputOnEnterPressed, self)
self._inputLeftNineSlice = NineSlice.New(frame.inputLeft)
self._inputRightNineSlice = NineSlice.New(frame.inputRight)
self._leftValue = nil
self._rightValue = nil
self._minValue = nil
self._maxValue = nil
self._dragging = nil
end
function Slider.Release(self)
self._leftValue = nil
self._rightValue = nil
self._minValue = nil
self._maxValue = nil
self._dragging = nil
self.__super:Release()
end
--- Set the extends of the possible range.
-- @tparam Slider self The slider object
-- @tparam number minValue The minimum value
-- @tparam number maxValue The maxmimum value
-- @treturn Slider The slider object
function Slider.SetRange(self, minValue, maxValue)
self._minValue = minValue
self._maxValue = maxValue
self._leftValue = minValue
self._rightValue = maxValue
return self
end
--- Sets the current value.
-- @tparam Slider self The slider object
-- @tparam number leftValue The lower end of the range
-- @tparam number rightValue The upper end of the range
-- @treturn Slider The slider object
function Slider.SetValue(self, leftValue, rightValue)
assert(leftValue < rightValue and leftValue >= self._minValue and rightValue <= self._maxValue)
self._leftValue = leftValue
self._rightValue = rightValue
return self
end
--- Gets the current value
-- @tparam Slider self The slider object
-- @treturn number The lower end of the range
-- @treturn number The upper end of the range
function Slider.GetValue(self)
return self._leftValue, self._rightValue
end
function Slider.Draw(self)
self.__super:Draw()
local frame = self:_GetBaseFrame()
local inputColor = Theme.GetColor("ACTIVE_BG")
self._inputLeftNineSlice:SetStyle("rounded")
self._inputRightNineSlice:SetStyle("rounded")
self._inputLeftNineSlice:SetVertexColor(inputColor:GetFractionalRGBA())
self._inputRightNineSlice:SetVertexColor(inputColor:GetFractionalRGBA())
local sliderHeight = self:_GetDimension("HEIGHT") / 2
local width = self:_GetDimension("WIDTH") - INPUT_AREA_SPACE
local leftPos = Math.Scale(self._leftValue, self._minValue, self._maxValue, 0, width - THUMB_WIDTH)
local rightPos = Math.Scale(self._rightValue, self._minValue, self._maxValue, 0, width - THUMB_WIDTH)
local fontPath, fontHeight = Theme.GetFont("BODY_BODY1"):GetWowFont()
local textColor = Theme.GetColor("TEXT")
-- wow renders the font slightly bigger than the designs would indicate, so subtract one from the font height
frame.inputRight:SetFont(fontPath, fontHeight)
frame.inputRight:SetTextColor(textColor:GetFractionalRGBA())
frame.inputRight:SetPoint("RIGHT", 0)
frame.inputRight:SetNumber(self._rightValue)
frame.dash:SetFont(fontPath, fontHeight)
frame.dash:SetTextColor(textColor:GetFractionalRGBA())
frame.dash:SetText("-")
frame.dash:SetPoint("RIGHT", frame.inputRight, "LEFT", 0, 0)
-- wow renders the font slightly bigger than the designs would indicate, so subtract one from the font height
frame.inputLeft:SetFont(fontPath, fontHeight)
frame.inputLeft:SetTextColor(textColor:GetFractionalRGBA())
frame.inputLeft:SetPoint("RIGHT", frame.dash, "LEFT", 0)
frame.inputLeft:SetNumber(self._leftValue)
frame.barTexture:ClearAllPoints()
frame.barTexture:SetPoint("LEFT", 0, 0)
frame.barTexture:SetPoint("RIGHT", frame.inputLeft, "LEFT", -16, 0)
frame.barTexture:SetHeight(sliderHeight / 3)
frame.barTexture:SetColorTexture(Theme.GetColor("FRAME_BG"):GetFractionalRGBA())
TSM.UI.TexturePacks.SetTextureAndSize(frame.thumbTextureLeft, "iconPack.14x14/Circle")
frame.thumbTextureLeft:SetPoint("LEFT", frame.barTexture, leftPos, 0)
TSM.UI.TexturePacks.SetTextureAndSize(frame.thumbTextureRight, "iconPack.14x14/Circle")
frame.thumbTextureRight:SetPoint("LEFT", frame.barTexture, rightPos, 0)
frame.activeBarTexture:SetPoint("LEFT", frame.thumbTextureLeft, "CENTER")
frame.activeBarTexture:SetPoint("RIGHT", frame.thumbTextureRight, "CENTER")
frame.activeBarTexture:SetHeight(sliderHeight / 3)
frame.activeBarTexture:SetColorTexture(Theme.GetColor("TEXT"):GetFractionalRGBA())
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Slider._GetCursorPositionValue(self)
local frame = self:_GetBaseFrame()
local x = GetCursorPosition() / frame:GetEffectiveScale()
local left = frame:GetLeft() + THUMB_WIDTH / 2
local right = frame:GetRight() - THUMB_WIDTH - INPUT_AREA_SPACE * 2 / 2
x = min(max(x, left), right)
local value = Math.Scale(x, left, right, self._minValue, self._maxValue)
return min(max(Math.Round(value), self._minValue), self._maxValue)
end
function Slider._UpdateLeftValue(self, value)
local newValue = max(min(value, self._rightValue), self._minValue)
if newValue == self._leftValue then
return
end
self._leftValue = newValue
self:Draw()
end
function Slider._UpdateRightValue(self, value)
local newValue = min(max(value, self._leftValue), self._maxValue)
if newValue == self._rightValue then
return
end
self._rightValue = newValue
self:Draw()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.InputOnEscapePressed(input)
input:ClearFocus()
end
function private.LeftInputOnEnterPressed(self)
local input = self:_GetBaseFrame().inputLeft
self:_UpdateLeftValue(input:GetNumber())
end
function private.RightInputOnEnterPressed(self)
local input = self:_GetBaseFrame().inputRight
self:_UpdateRightValue(input:GetNumber())
end
function private.FrameOnMouseDown(self)
local frame = self:_GetBaseFrame()
frame.inputLeft:ClearFocus()
frame.inputRight:ClearFocus()
local value = self:_GetCursorPositionValue()
local leftDiff = abs(value - self._leftValue)
local rightDiff = abs(value - self._rightValue)
if value < self._leftValue then
-- clicked to the left of the left thumb, so drag that
self._dragging = "left"
elseif value > self._rightValue then
-- clicked to the right of the right thumb, so drag that
self._dragging = "right"
elseif self._leftValue == self._rightValue then
-- just ignore this click since they clicked on both thumbs
elseif leftDiff < rightDiff then
-- clicked closer to the left thumb, so drag that
self._dragging = "left"
else
-- clicked closer to the right thumb (or right in the middle), so drag that
self._dragging = "right"
end
end
function private.FrameOnMouseUp(self)
self._dragging = nil
end
function private.FrameOnUpdate(self)
if not self._dragging then
return
end
if self._dragging == "left" then
self:_UpdateLeftValue(self:_GetCursorPositionValue())
elseif self._dragging == "right" then
self:_UpdateRightValue(self:_GetCursorPositionValue())
else
error("Unexpected dragging: "..tostring(self._dragging))
end
end

View File

@@ -0,0 +1,128 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- SniperScrollingTable UI Element Class.
-- A special shopping scrolling table used for sniper which has an extra icon column on the left. It is a subclass of
-- the @{AuctionScrollingTable} class.
-- @classmod SniperScrollingTable
local _, TSM = ...
local SniperScrollingTable = TSM.Include("LibTSMClass").DefineClass("SniperScrollingTable", TSM.UI.AuctionScrollingTable)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(SniperScrollingTable)
TSM.UI.SniperScrollingTable = SniperScrollingTable
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function SniperScrollingTable.__init(self)
self.__super:__init()
self._highestBrowseId = 0
self._onRowRemovedHandler = nil
end
function SniperScrollingTable.Acquire(self)
self.__super:Acquire()
self:GetScrollingTableInfo()
:NewColumn("icon", true)
:SetTitleIcon("iconPack.14x14/Attention")
:SetIconSize(14)
:SetIconHoverEnabled(true)
:SetIconClickHandler(private.RemoveIconClickHandler)
:SetIconFunction(private.RemoveIconFunction)
:SetJustifyH("CENTER")
:SetFont("BODY_BODY3")
:Commit()
:RemoveColumn("timeLeft")
:Commit()
if TSM.IsWowClassic() then
self._sortCol = "icon"
self._sortAscending = true
end
self._highestBrowseId = 0
end
function SniperScrollingTable.Release(self)
self._onRowRemovedHandler = nil
self.__super:Release()
end
--- Registers a script handler.
-- @tparam SniperScrollingTable self The sniper scrolling table object
-- @tparam string script The script to register for (supported scripts: `OnRowRemoved`)
-- @tparam function handler The script handler which will be called with the sniper scrolling table object followed by
-- any arguments to the script
-- @treturn SniperScrollingTable The sniper scrolling table object
function SniperScrollingTable.SetScript(self, script, handler)
if script == "OnRowRemoved" then
self._onRowRemovedHandler = handler
else
self.__super:SetScript(script, handler)
end
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function SniperScrollingTable._UpdateData(self, queryChanged)
self.__super:_UpdateData(queryChanged)
self._highestBrowseId = 0
for _, row in ipairs(self._data) do
if row:IsSubRow() then
local _, _, browseId = row:GetListingInfo()
self._highestBrowseId = max(self._highestBrowseId, browseId or 0)
end
end
end
function SniperScrollingTable._GetSortValue(self, row, id, isAscending)
if id == "icon" then
if not row:IsSubRow() then
return 0
end
local _, _, browseId = row:GetListingInfo()
return -browseId
else
return self.__super:_GetSortValue(row, id, isAscending)
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.RemoveIconClickHandler(self, subRow)
if not subRow:IsSubRow() then
local baseItemString = subRow:GetBaseItemString()
subRow = self._firstSubRowByItem[baseItemString] or subRow
end
if self._onRowRemovedHandler then
self:_onRowRemovedHandler(subRow)
end
end
function private.RemoveIconFunction(self, row, isMouseOver)
if isMouseOver then
return "iconPack.14x14/Close/Default"
end
local isRecent = true
if row:IsSubRow() then
local _, _, browseId = row:GetListingInfo()
isRecent = self._highestBrowseId == browseId
end
return isRecent and "iconPack.14x14/Attention" or "iconPack.14x14/Close/Default"
end

106
Core/UI/Elements/Spacer.lua Normal file
View File

@@ -0,0 +1,106 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Spacer UI Element Class.
-- A spacer is a light-weight element which doesn't have any content but can be used to assist with layouts. It is a
-- subclass of the @{Element} class.
-- @classmod Spacer
local _, TSM = ...
local Spacer = TSM.Include("LibTSMClass").DefineClass("Spacer", TSM.UI.Element)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Spacer)
TSM.UI.Spacer = Spacer
-- ============================================================================
-- Fake Frame Methods
-- ============================================================================
local FAKE_FRAME_MT = {
__index = {
SetParent = function(self, parent)
self._parent = parent
end,
GetParent = function(self)
return self._parent
end,
SetScale = function(self, scale)
self._scale = scale
end,
GetScale = function(self)
return self._scale
end,
SetWidth = function(self, width)
self._width = width
end,
GetWidth = function(self)
return self._width
end,
SetHeight = function(self, height)
self._height = height
end,
GetHeight = function(self)
return self._height
end,
Show = function(self)
self._visible = true
end,
Hide = function(self)
self._visible = false
end,
IsVisible = function(self)
return self._visible
end,
ClearAllPoints = function(self)
-- do nothing
end,
SetPoint = function(self, ...)
-- do nothing
end,
},
}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Spacer.__init(self)
self.__super:__init(self)
local fakeFrame = {
_parent = nil,
_scale = 1,
_width = 0,
_height = 0,
_visible = false,
}
self._fakeFrame = setmetatable(fakeFrame, FAKE_FRAME_MT)
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Spacer._GetBaseFrame(self)
return self._fakeFrame
end

View File

@@ -0,0 +1,113 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- TabGroup UI Element Class.
-- A tab group uses text and a horizontal line to denote the tabs, with coloring indicating the one which is selected.
-- It is a subclass of the @{ViewContainer} class.
-- @classmod TabGroup
local _, TSM = ...
local TabGroup = TSM.Include("LibTSMClass").DefineClass("TabGroup", TSM.UI.ViewContainer)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(TabGroup)
TSM.UI.TabGroup = TabGroup
local private = {}
local BUTTON_HEIGHT = 24
local BUTTON_PADDING_BOTTOM = 4
local LINE_THICKNESS = 2
local LINE_THICKNESS_SELECTED = 2
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function TabGroup.__init(self)
self.__super:__init()
self._buttons = {}
end
function TabGroup.Acquire(self)
self.__super.__super:AddChildNoLayout(UIElements.New("Frame", "buttons")
:SetLayout("HORIZONTAL")
:AddAnchor("TOPLEFT")
:AddAnchor("TOPRIGHT")
)
self.__super:Acquire()
end
function TabGroup.Release(self)
wipe(self._buttons)
self.__super:Release()
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function TabGroup._GetContentPadding(self, side)
if side == "TOP" then
return BUTTON_HEIGHT + BUTTON_PADDING_BOTTOM + LINE_THICKNESS
end
return self.__super:_GetContentPadding(side)
end
function TabGroup.Draw(self)
self.__super.__super.__super:Draw()
local selectedPath = self:GetPath()
local buttons = self:GetElement("buttons")
buttons:SetHeight(BUTTON_HEIGHT + BUTTON_PADDING_BOTTOM + LINE_THICKNESS)
buttons:ReleaseAllChildren()
for i, buttonPath in ipairs(self._pathsList) do
local isSelected = buttonPath == selectedPath
buttons:AddChild(UIElements.New("Frame", self._id.."_Tab"..i)
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Button", "button")
:SetMargin(0, 0, 0, BUTTON_PADDING_BOTTOM)
:SetFont("BODY_BODY1_BOLD")
:SetJustifyH("CENTER")
:SetTextColor(isSelected and "INDICATOR" or "TEXT_ALT")
:SetContext(self)
:SetText(buttonPath)
:SetScript("OnEnter", not isSelected and private.OnButtonEnter)
:SetScript("OnLeave", not isSelected and private.OnButtonLeave)
:SetScript("OnClick", private.OnButtonClicked)
)
:AddChild(UIElements.New("Texture", "line")
:SetHeight(isSelected and LINE_THICKNESS_SELECTED or LINE_THICKNESS)
:SetTexture(isSelected and "INDICATOR" or "TEXT_ALT")
)
)
end
self.__super:Draw()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnButtonEnter(button)
button:SetTextColor("TEXT")
:Draw()
end
function private.OnButtonLeave(button)
button:SetTextColor("TEXT_ALT")
:Draw()
end
function private.OnButtonClicked(button)
local self = button:GetContext()
local path = button:GetText()
self:SetPath(path, self:GetPath() ~= path)
end

217
Core/UI/Elements/Text.lua Normal file
View File

@@ -0,0 +1,217 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Text UI Element Class.
-- A text element simply holds a text string. It is a subclass of the @{Element} class.
-- @classmod Text
local _, TSM = ...
local Text = TSM.Include("LibTSMClass").DefineClass("Text", TSM.UI.Element)
local Color = TSM.Include("Util.Color")
local Theme = TSM.Include("Util.Theme")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Text)
TSM.UI.Text = Text
local STRING_RIGHT_PADDING = 4
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Text.__init(self, frame)
frame = frame or UIElements.CreateFrame(self, "Frame")
self.__super:__init(frame)
frame.text = UIElements.CreateFontString(self, frame)
self._textStr = ""
self._autoWidth = false
self._textColor = "TEXT"
self._font = nil
self._justifyH = "LEFT"
self._justifyV = "MIDDLE"
end
function Text.Release(self)
self._textStr = ""
self._autoWidth = false
self._textColor = "TEXT"
self._font = nil
self._justifyH = "LEFT"
self._justifyV = "MIDDLE"
self:_GetBaseFrame().text:SetSpacing(0)
self.__super:Release()
end
--- Sets the width of the text.
-- @tparam Text self The text object
-- @tparam ?number|string width The width of the text, "AUTO" to set the width based on the length
-- of the text, or nil to have an undefined width
-- @treturn Text The text object
function Text.SetWidth(self, width)
if width == "AUTO" then
self._autoWidth = true
else
self._autoWidth = false
self.__super:SetWidth(width)
end
return self
end
--- Sets the font.
-- @tparam Text self The text object
-- @tparam string font The font key
-- @treturn Text The text object
function Text.SetFont(self, font)
assert(Theme.GetFont(font))
self._font = font
return self
end
--- Sets the color of the text.
-- @tparam Text self The text object
-- @tparam Color|string color The text color as a Color object or a theme color key
-- @treturn Text The text object
function Text.SetTextColor(self, color)
assert((type(color) == "string" and Theme.GetColor(color)) or Color.IsInstance(color))
self._textColor = color
return self
end
--- Sets the horizontal justification of the text.
-- @tparam Text self The text object
-- @tparam string justifyH The horizontal justification (either "LEFT", "CENTER" or "RIGHT")
-- @treturn Text The text object
function Text.SetJustifyH(self, justifyH)
assert(justifyH == "LEFT" or justifyH == "CENTER" or justifyH == "RIGHT")
self._justifyH = justifyH
return self
end
--- Sets the vertical justification of the text.
-- @tparam Text self The text object
-- @tparam string justifyV The vertical justification (either "TOP", "MIDDLE" or "BOTTOM")
-- @treturn Text The text object
function Text.SetJustifyV(self, justifyV)
assert(justifyV == "TOP" or justifyV == "MIDDLE" or justifyV == "BOTTOM")
self._justifyV = justifyV
return self
end
--- Set the text.
-- @tparam Text self The text object
-- @tparam ?string|number text The text
-- @treturn Text The text object
function Text.SetText(self, text)
if type(text) == "number" then
text = tostring(text)
end
assert(type(text) == "string")
self._textStr = text
return self
end
--- Set formatted text.
-- @tparam Text self The text object
-- @tparam vararg ... The format string and parameters
-- @treturn Text The text object
function Text.SetFormattedText(self, ...)
self:SetText(format(...))
return self
end
--- Gets the text string.
-- @tparam Text self The text object
-- @treturn string The text string
function Text.GetText(self)
return self._textStr
end
--- Get the rendered text string width.
-- @tparam Text self The text object
-- @treturn number The rendered text string width
function Text.GetStringWidth(self)
local text = self:_GetBaseFrame().text
self:_ApplyFont()
text:SetText(self._textStr)
return text:GetStringWidth()
end
--- Get the rendered text string height.
-- @tparam Text self The text object
-- @treturn number The rendered text string height
function Text.GetStringHeight(self)
local text = self:_GetBaseFrame().text
self:_ApplyFont()
text:SetText(self._textStr)
return text:GetStringHeight()
end
function Text.Draw(self)
self.__super:Draw()
local text = self:_GetBaseFrame().text
text:ClearAllPoints()
text:SetAllPoints()
-- set the font
self:_ApplyFont()
-- set the justification
text:SetJustifyH(self._justifyH)
text:SetJustifyV(self._justifyV)
-- set the text color
text:SetTextColor(self:_GetTextColor():GetFractionalRGBA())
-- set the text
text:SetText(self._textStr)
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Text._GetTextColor(self)
if type(self._textColor) == "string" then
return Theme.GetColor(self._textColor)
else
assert(Color.IsInstance(self._textColor))
return self._textColor
end
end
function Text._GetMinimumDimension(self, dimension)
if dimension == "WIDTH" and self._autoWidth then
return 0, self._width == nil
else
return self.__super:_GetMinimumDimension(dimension)
end
end
function Text._GetPreferredDimension(self, dimension)
if dimension == "WIDTH" and self._autoWidth then
return self:GetStringWidth() + STRING_RIGHT_PADDING
else
return self.__super:_GetPreferredDimension(dimension)
end
end
function Text._ApplyFont(self)
local text = self:_GetBaseFrame().text
local font = Theme.GetFont(self._font)
text:SetFont(font:GetWowFont())
-- There's a Blizzard bug where spacing incorrectly gets applied to embedded textures, so just set it to 0 in that case
-- TODO: come up with a better fix if we need multi-line text with embedded textures
if strfind(self._textStr, "\124T") then
text:SetSpacing(0)
else
text:SetSpacing(font:GetSpacing())
end
end

View File

@@ -0,0 +1,106 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Texture UI Element Class.
-- This is a simple, light-weight element which is used to display a texture. It is a subclass of the @{Element} class.
-- @classmod Texture
local _, TSM = ...
local Texture = TSM.Include("LibTSMClass").DefineClass("Texture", TSM.UI.Element)
local Color = TSM.Include("Util.Color")
local Theme = TSM.Include("Util.Theme")
local ItemInfo = TSM.Include("Service.ItemInfo")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Texture)
TSM.UI.Texture = Texture
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Texture.__init(self)
local texture = UIParent:CreateTexture()
-- hook SetParent/GetParent since textures can't have a nil parent
texture._oldSetParent = texture.SetParent
texture.SetParent = private.SetParent
texture.GetParent = private.GetParent
self.__super:__init(texture)
self._texture = nil
end
function Texture.Release(self)
self._texture = nil
self.__super:Release()
end
--- Sets the texture.
-- @tparam Texture self The texture object
-- @tparam ?string|number texture Either a texture pack string, itemString, WoW file id, or theme color key
-- @treturn Texture The texture object
function Texture.SetTexture(self, texture)
self._texture = texture
return self
end
--- Sets the texture and size based on a texture pack string.
-- @tparam Texture self The texture object
-- @tparam string texturePack A texture pack string
-- @treturn Texture The texture object
function Texture.SetTextureAndSize(self, texturePack)
self:SetTexture(texturePack)
self:SetSize(TSM.UI.TexturePacks.GetSize(texturePack))
return self
end
function Texture.Draw(self)
self.__super:Draw()
local texture = self:_GetBaseFrame()
texture:SetTexture(nil)
texture:SetTexCoord(0, 1, 0, 1)
texture:SetVertexColor(1, 1, 1, 1)
if type(self._texture) == "string" and TSM.UI.TexturePacks.IsValid(self._texture) then
-- this is a texture pack
TSM.UI.TexturePacks.SetTexture(texture, self._texture)
elseif type(self._texture) == "string" and strmatch(self._texture, "^[ip]:%d+") then
-- this is an itemString
texture:SetTexture(ItemInfo.GetTexture(self._texture))
elseif type(self._texture) == "string" then
-- this is a theme color key
texture:SetColorTexture(Theme.GetColor(self._texture):GetFractionalRGBA())
elseif type(self._texture) == "number" then
-- this is a wow file id
texture:SetTexture(self._texture)
elseif Color.IsInstance(self._texture) then
texture:SetColorTexture(self._texture:GetFractionalRGBA())
else
error("Invalid texture: "..tostring(self._texture))
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.SetParent(self, parent)
self._parent = parent
if parent then
self:Show()
else
self:Hide()
end
self:_oldSetParent(parent or UIParent)
end
function private.GetParent(self)
return self._parent
end

180
Core/UI/Elements/Toggle.lua Normal file
View File

@@ -0,0 +1,180 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Toggle UI Element Class.
-- A toggle element allows the user to select between a fixed set of options. It is a subclass of the @{Container} class.
-- @classmod Toggle
local _, TSM = ...
local Toggle = TSM.Include("LibTSMClass").DefineClass("Toggle", TSM.UI.Container)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Toggle)
TSM.UI.Toggle = Toggle
local private = {}
local BUTTON_PADDING = 16
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Toggle.__init(self)
local frame = UIElements.CreateFrame(self, "Frame")
self.__super:__init(frame)
self._optionsList = {}
self._buttons = {}
self._onValueChangedHandler = nil
self._selectedOption = nil
self._booleanKey = nil
self._font = "BODY_BODY3"
end
function Toggle.Release(self)
wipe(self._optionsList)
wipe(self._buttons)
self._onValueChangedHandler = nil
self._selectedOption = nil
self._booleanKey = nil
self._font = "BODY_BODY3"
self.__super:Release()
end
--- Add an option.
-- @tparam Toggle self The toggle object
-- @tparam string option The text that goes with the option
-- @tparam boolean setSelected Whether or not to set this as the selected option
-- @treturn Toggle The toggle object
function Toggle.AddOption(self, option, setSelected)
tinsert(self._optionsList, option)
if setSelected then
self:SetOption(option)
end
return self
end
--- Sets the currently selected option.
-- @tparam Toggle self The toggle object
-- @tparam string option The selected option
-- @tparam boolean redraw Whether or not to redraw the toggle
-- @treturn Toggle The toggle object
function Toggle.SetOption(self, option, redraw)
if option ~= self._selectedOption then
self._selectedOption = option
if self._onValueChangedHandler then
self:_onValueChangedHandler(option)
end
end
if redraw then
self:Draw()
end
return self
end
--- Clears the currently selected option.
-- @tparam Toggle self The toggle object
-- @tparam boolean redraw Whether or not to redraw the toggle
-- @treturn Toggle The toggle object
function Toggle.ClearOption(self, redraw)
self._selectedOption = nil
if redraw then
self:Draw()
end
return self
end
--- Sets whether or not the toggle is disabled.
-- @tparam Toggle self The toggle object
-- @tparam boolean disabled Whether or not the toggle is disabled
-- @treturn Toggle The toggle object
function Toggle.SetDisabled(self, disabled)
self._disabled = disabled
return self
end
--- Registers a script handler.
-- @tparam Toggle self The toggle object
-- @tparam string script The script to register for (supported scripts: `OnValueChanged`)
-- @tparam function handler The script handler which will be called with the toggle object followed by any arguments to
-- the script
-- @treturn Toggle The toggle object
function Toggle.SetScript(self, script, handler)
if script == "OnValueChanged" then
self._onValueChangedHandler = handler
else
error("Unknown Toggle script: "..tostring(script))
end
return self
end
function Toggle.SetFont(self, font)
self._font = font
return self
end
--- Get the selected option.
-- @tparam Toggle self The toggle object
-- @treturn string The selected option
function Toggle.GetValue(self)
return self._selectedOption
end
function Toggle.Draw(self)
self.__super.__super:Draw()
-- add new buttons if necessary
while #self._buttons < #self._optionsList do
local num = #self._buttons + 1
local button = UIElements.New("Checkbox", self._id.."_Button"..num)
:SetFont(self._font)
:SetScript("OnValueChanged", private.ButtonOnClick)
self:AddChildNoLayout(button)
tinsert(self._buttons, button)
end
local selectedPath = self._selectedOption
local height = self:_GetDimension("HEIGHT")
local buttonWidth = (self:_GetDimension("WIDTH") / #self._buttons) + BUTTON_PADDING
local offsetX = 0
for i, button in ipairs(self._buttons) do
local buttonPath = self._optionsList[i]
if i <= #self._optionsList then
button:SetFont(self._font)
button:SetWidth("AUTO")
button:SetTheme("RADIO")
button:SetCheckboxPosition("LEFT")
button:SetText(buttonPath)
button:SetSize(buttonWidth, height)
button:SetDisabled(self._disabled)
button:WipeAnchors()
button:AddAnchor("TOPLEFT", offsetX, 0)
offsetX = offsetX + buttonWidth
else
button:Hide()
end
if buttonPath == selectedPath then
button:SetChecked(true, true)
else
button:SetChecked(false, true)
end
end
self.__super:Draw()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.ButtonOnClick(button)
local self = button:GetParentElement()
self:SetOption(button:GetText(), true)
end

View File

@@ -0,0 +1,197 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- ToggleOnOff UI Element Class.
-- This is a simple on/off toggle which uses different textures for the different states. It is a subclass of the
-- @{Container} class.
-- @classmod ToggleOnOff
local _, TSM = ...
local ToggleOnOff = TSM.Include("LibTSMClass").DefineClass("ToggleOnOff", TSM.UI.Container)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(ToggleOnOff)
TSM.UI.ToggleOnOff = ToggleOnOff
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ToggleOnOff.__init(self)
local frame = UIElements.CreateFrame(self, "Frame")
self.__super:__init(frame)
self._value = false
self._disabled = false
self._settingTable = nil
self._settingKey = nil
self._onValueChangedHandler = nil
end
function ToggleOnOff.Acquire(self)
local frame = self:_GetBaseFrame()
self:AddChildNoLayout(UIElements.New("Frame", "toggle")
:SetLayout("HORIZONTAL")
:AddAnchor("TOPLEFT", frame)
:AddAnchor("BOTTOMRIGHT", frame)
:SetContext(self)
:AddChild(UIElements.New("Checkbox", "yes")
:SetWidth("AUTO")
:SetTheme("RADIO")
:SetFont("BODY_BODY2")
:SetText(YES)
:SetCheckboxPosition("LEFT")
:SetScript("OnValueChanged", private.OnYesClickHandler)
)
:AddChild(UIElements.New("Checkbox", "no")
:SetWidth("AUTO")
:SetTheme("RADIO")
:SetFont("BODY_BODY2")
:SetMargin(8, 0, 0, 0)
:SetText(NO)
:SetCheckboxPosition("LEFT")
:SetScript("OnValueChanged", private.OnNoClickHandler)
)
:AddChild(UIElements.New("Spacer", "spacer"))
)
self.__super:Acquire()
end
function ToggleOnOff.Release(self)
self._value = false
self._disabled = false
self._settingTable = nil
self._settingKey = nil
self._onValueChangedHandler = nil
--self:_GetBaseFrame():Enable()
self.__super:Release()
end
--- Sets the setting info.
-- This method is used to have the value of the toggle automatically correspond with the value of a field in a table.
-- This is useful for toggles which are tied directly to settings.
-- @tparam ToggleOnOff self The toggles object
-- @tparam table tbl The table which the field to set belongs to
-- @tparam string key The key into the table to be set based on the toggle's state
-- @treturn ToggleOnOff The toggles object
function ToggleOnOff.SetSettingInfo(self, tbl, key)
self._settingTable = tbl
self._settingKey = key
self._value = tbl[key]
return self
end
--- Sets whether or not the toggle is disabled.
-- @tparam ToggleOnOff self The toggles object
-- @tparam boolean disabled Whether or not the toggle is disabled
-- @tparam boolean redraw Whether or not to redraw the toggle
-- @treturn ToggleOnOff The toggles object
function ToggleOnOff.SetDisabled(self, disabled, redraw)
self._disabled = disabled
if disabled then
self:GetElement("toggle.yes"):SetDisabled(true)
self:GetElement("toggle.no"):SetDisabled(true)
else
self:GetElement("toggle.yes"):SetDisabled(false)
self:GetElement("toggle.no"):SetDisabled(false)
end
if redraw then
self:Draw()
end
return self
end
--- Set the value of the toggle.
-- @tparam ToggleOnOff self The toggles object
-- @tparam boolean value Whether the value is on (true) or off (false)
-- @tparam boolean redraw Whether or not to redraw the toggle
-- @treturn ToggleOnOff The toggles object
function ToggleOnOff.SetValue(self, value, redraw)
if value ~= self._value then
self._value = value
if self._settingTable then
self._settingTable[self._settingKey] = value
end
if self._onValueChangedHandler then
self:_onValueChangedHandler(value)
end
end
if redraw then
self:Draw()
end
return self
end
--- Registers a script handler.
-- @tparam ToggleOnOff self The toggles object
-- @tparam string script The script to register for (supported scripts: `OnValueChanged`)
-- @tparam function handler The script handler which will be called with the toggles object followed by any
-- arguments to the script
-- @treturn ToggleOnOff The toggles object
function ToggleOnOff.SetScript(self, script, handler)
if script == "OnValueChanged" then
self._onValueChangedHandler = handler
else
error("Unknown ToggleOnOff script: "..tostring(script))
end
return self
end
--- Get the value of the toggle.
-- @tparam ToggleOnOff self The toggles object
-- @treturn boolean The value of the toggle
function ToggleOnOff.GetValue(self)
return self._value
end
function ToggleOnOff.Draw(self)
if self._value then
self:GetElement("toggle.yes"):SetChecked(true, true)
self:GetElement("toggle.no"):SetChecked(false, true)
else
self:GetElement("toggle.yes"):SetChecked(false, true)
self:GetElement("toggle.no"):SetChecked(true, true)
end
if self._disabled then
self:GetElement("toggle.yes"):SetDisabled(true)
self:GetElement("toggle.no"):SetDisabled(true)
else
self:GetElement("toggle.yes"):SetDisabled(false)
self:GetElement("toggle.no"):SetDisabled(false)
end
self.__super:Draw()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnYesClickHandler(button)
if not button:IsChecked() then
button:SetChecked(true, true)
return
end
local self = button:GetParentElement():GetContext()
self:SetValue(true, true)
end
function private.OnNoClickHandler(button)
if not button:IsChecked() then
button:SetChecked(true, true)
return
end
local self = button:GetParentElement():GetContext()
self:SetValue(false, true)
end

View File

@@ -0,0 +1,238 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- ViewContainer UI Element Class.
-- A view container allows the content to be changed depending on the selected view (called the path). It is a subclass of the @{Container} class.
-- @classmod ViewContainer
local _, TSM = ...
local ViewContainer = TSM.Include("LibTSMClass").DefineClass("ViewContainer", TSM.UI.Container)
local Table = TSM.Include("Util.Table")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(ViewContainer)
TSM.UI.ViewContainer = ViewContainer
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ViewContainer.__init(self)
local frame = UIElements.CreateFrame(self, "Frame")
self.__super:__init(frame)
self._pathsList = {}
self._contextTable = nil
self._defaultContextTable = nil
end
function ViewContainer.Acquire(self)
self._path = nil
self._navCallback = nil
self.__super:Acquire()
end
function ViewContainer.Release(self)
wipe(self._pathsList)
self.__super:Release()
self._contextTable = nil
self._defaultContextTable = nil
end
function ViewContainer.SetLayout(self, layout)
error("ViewContainer doesn't support this method")
end
function ViewContainer.AddChild(self, child)
error("ViewContainer doesn't support this method")
end
function ViewContainer.AddChildNoLayout(self, child)
error("ViewContainer doesn't support this method")
end
--- Set the navigation callback.
-- @tparam ViewContainer self The view container object
-- @tparam function callback The function called when the selected path changes to get the new content
-- @treturn ViewContainer The view container object
function ViewContainer.SetNavCallback(self, callback)
self._navCallback = callback
return self
end
--- Add a path (view).
-- @tparam ViewContainer self The view container object
-- @tparam string path The path
-- @tparam[opt=false] boolean setSelected Set this as the selected path (view)
-- @treturn ViewContainer The view container object
function ViewContainer.AddPath(self, path, setSelected)
tinsert(self._pathsList, path)
if self._contextTable then
assert(setSelected == nil, "Cannot set selected path when using a context table")
local newPathIndex = Table.KeyByValue(self._pathsList, path)
if self._contextTable.pathIndex == newPathIndex then
self:SetPath(path)
end
elseif setSelected then
self:SetPath(path)
end
return self
end
--- Renames a path (view).
-- @tparam ViewContainer self The view container object
-- @tparam string path The new path
-- @tparam number index The index of the path to change
-- @treturn ViewContainer The view container object
function ViewContainer.RenamePath(self, path, index)
local changePath = self._pathsList[index] == self._path
self._pathsList[index] = path
if changePath then
self:SetPath(path)
end
return self
end
--- Set the selected path (view).
-- @tparam ViewContainer self The view container object
-- @tparam string path The selected path
-- @tparam boolean redraw Whether or not to redraw the view container
-- @treturn ViewContainer The view container object
function ViewContainer.SetPath(self, path, redraw)
if path ~= self._path then
local child = self:_GetChild()
if child then
assert(#self._layoutChildren == 1)
self:RemoveChild(child)
child:Release()
end
self.__super:AddChild(self:_navCallback(path))
self._path = path
-- Save the path index of the new selected path to the context table
if self._contextTable then
self._contextTable.pathIndex = Table.KeyByValue(self._pathsList, path)
end
end
if redraw then
self:Draw()
end
return self
end
--- Reload the current view.
-- @tparam ViewContainer self The view container object
function ViewContainer.ReloadContent(self)
local path = self._path
self._path = nil
self:SetPath(path, true)
end
--- Get the current path (view).
-- @tparam ViewContainer self The view container object
-- @treturn string The current path
function ViewContainer.GetPath(self)
return self._path
end
--- Get a list of the paths for the view container.
-- @tparam ViewContainer self The view container object
-- @treturn table The path list
function ViewContainer.GetPathList(self)
return self._pathsList
end
function ViewContainer.Draw(self)
self.__super.__super:Draw()
local child = self:_GetChild()
local childFrame = child:_GetBaseFrame()
-- set the child to be full-size
childFrame:ClearAllPoints()
local xOffset, yOffset = child:_GetMarginAnchorOffsets("BOTTOMLEFT")
local paddingXOffset, paddingYOffset = self:_GetPaddingAnchorOffsets("BOTTOMLEFT")
xOffset = xOffset + paddingXOffset - self:_GetContentPadding("LEFT")
yOffset = yOffset + paddingYOffset - self:_GetContentPadding("BOTTOM")
childFrame:SetPoint("BOTTOMLEFT", xOffset, yOffset)
xOffset, yOffset = child:_GetMarginAnchorOffsets("TOPRIGHT")
paddingXOffset, paddingYOffset = self:_GetPaddingAnchorOffsets("TOPRIGHT")
xOffset = xOffset + paddingXOffset - self:_GetContentPadding("RIGHT")
yOffset = yOffset + paddingYOffset - self:_GetContentPadding("TOP")
childFrame:SetPoint("TOPRIGHT", xOffset, yOffset)
child:Draw()
-- draw the no-layout children
for _, noLayoutChild in ipairs(self._noLayoutChildren) do
noLayoutChild:Draw()
end
end
--- Sets the context table.
-- This table can be used to save which tab is active, refrenced by the path index
-- @tparam ViewContainer self The view container object
-- @tparam table tbl The context table
-- @tparam table defaultTbl Default values
-- @treturn ViewContainer The view container object
function ViewContainer.SetContextTable(self, tbl, defaultTbl)
assert(defaultTbl.pathIndex ~= nil)
tbl.pathIndex = tbl.pathIndex or defaultTbl.pathIndex
self._contextTable = tbl
self._defaultContextTable = defaultTbl
return self
end
--- Sets the context table from a settings object.
-- @tparam ViewContainer self The view container object
-- @tparam Settings settings The settings object
-- @tparam string key The setting key
-- @treturn ViewContainer The view container object
function ViewContainer.SetSettingsContext(self, settings, key)
return self:SetContextTable(settings[key], settings:GetDefaultReadOnly(key))
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function ViewContainer._GetMinimumDimension(self, dimension)
if dimension == "WIDTH" then
local width = self._width
if width then
return width, false
else
return self:_GetChild():_GetMinimumDimension(dimension)
end
elseif dimension == "HEIGHT" then
local height = self._height
if height then
return height, false
else
return self:_GetChild():_GetMinimumDimension(dimension)
end
else
error("Invalid dimension: "..tostring(dimension))
end
end
function ViewContainer._GetContentPadding(self, side)
if side == "TOP" then
return 0
elseif side == "BOTTOM" then
return 0
elseif side == "LEFT" then
return 0
elseif side == "RIGHT" then
return 0
else
error("Invalid side: "..tostring(side))
end
end
function ViewContainer._GetChild(self)
return self._layoutChildren[1]
end

339
Core/UI/FrameStack.lua Normal file
View File

@@ -0,0 +1,339 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- FrameStack Functions
-- @module FrameStack
local _, TSM = ...
local FrameStack = TSM.UI:NewPackage("FrameStack")
local Math = TSM.Include("Util.Math")
local Theme = TSM.Include("Util.Theme")
local Table = TSM.Include("Util.Table")
local Vararg = TSM.Include("Util.Vararg")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local UIElements = TSM.Include("UI.UIElements")
local private = {}
local STRATA_ORDER = {"TOOLTIP", "FULLSCREEN_DIALOG", "FULLSCREEN", "DIALOG", "HIGH", "MEDIUM", "LOW", "BACKGROUND", "WORLD"}
local framesByStrata = {
WORLD = {},
BACKGROUND = {},
LOW = {},
MEDIUM = {},
HIGH = {},
DIALOG = {},
FULLSCREEN = {},
FULLSCREEN_DIALOG = {},
TOOLTIP = {},
}
local ELEMENT_ATTR_KEYS = {
"_hScrollbar",
"_vScrollbar",
"_hScrollFrame",
"_hContent",
"_vScrollFrame",
"_content",
"_header",
"_frame",
"_rows",
}
local COLOR_KEYS = {
FRAME_BG = true,
PRIMARY_BG = true,
PRIMARY_BG_ALT = true,
ACTIVE_BG = true,
ACTIVE_BG_ALT = true,
INDICATOR = true,
INDICATOR_ALT = true,
INDICATOR_DISABLED = true,
TEXT = true,
TEXT_ALT = true,
TEXT_DISABLED = true,
}
local FEEDBACK_COLOR_KEYS = {
RED = true,
YELLOW = true,
GREEN = true,
BLUE = true,
ORANGE = true,
}
local FONT_KEYS = {
HEADING_H5 = true,
BODY_BODY1 = true,
BODY_BODY1_BOLD = true,
BODY_BODY2 = true,
BODY_BODY2_MEDIUM = true,
BODY_BODY2_BOLD = true,
BODY_BODY3 = true,
BODY_BODY3_MEDIUM = true,
ITEM_BODY1 = true,
ITEM_BODY2 = true,
ITEM_BODY3 = true,
TABLE_TABLE1 = true,
}
local ELEMENT_STYLE_KEYS = {
"_background",
"_texture",
"_backgroundColor",
"_borderColor",
"_textStr",
"_textColor",
"_font",
"_roundedCorners",
"_borderSize",
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function FrameStack.Toggle()
if not TSMFrameStackTooltip then
CreateFrame("GameTooltip", "TSMFrameStackTooltip", UIParent, "GameTooltipTemplate")
TSMFrameStackTooltip.highlightFrame = CreateFrame("Frame", nil, nil, TSM.IsShadowlands() and "BackdropTemplate" or nil)
TSMFrameStackTooltip.highlightFrame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
TSMFrameStackTooltip.highlightFrame:SetBackdropColor(1, 0, 0, 0.3)
TSMFrameStackTooltip:Hide()
ScriptWrapper.Set(TSMFrameStackTooltip, "OnUpdate", private.OnUpdate)
end
if TSMFrameStackTooltip:IsVisible() then
TSMFrameStackTooltip:Hide()
TSMFrameStackTooltip.highlightFrame:Hide()
else
TSMFrameStackTooltip.lastUpdate = 0
TSMFrameStackTooltip.altDown = nil
TSMFrameStackTooltip.index = 1
TSMFrameStackTooltip.numFrames = 0
TSMFrameStackTooltip:SetOwner(UIParent, "ANCHOR_NONE")
TSMFrameStackTooltip:SetPoint("TOPLEFT", 0, 0)
TSMFrameStackTooltip:AddLine("Loading...")
TSMFrameStackTooltip:Show()
TSMFrameStackTooltip.highlightFrame:Show()
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.OnUpdate(self)
if self.lastUpdate + 0.05 >= GetTime() then
return
end
self.lastUpdate = GetTime()
local numFrames = 0
for _, strata in ipairs(STRATA_ORDER) do
for _, strataFrame in ipairs(framesByStrata[strata]) do
local name = private.GetFrameName(strataFrame)
if not strmatch(name, "innerBorderFrame") then
numFrames = numFrames + 1
end
end
end
if numFrames ~= TSMFrameStackTooltip.numFrames then
TSMFrameStackTooltip.index = 1
TSMFrameStackTooltip.numFrames = numFrames
end
local leftAltDown = IsKeyDown("LALT")
local rightAltDown = IsKeyDown("RALT")
if not self.altDown and leftAltDown and not rightAltDown then
self.altDown = "LEFT"
if TSMFrameStackTooltip.index == TSMFrameStackTooltip.numFrames then
TSMFrameStackTooltip.index = 1
else
TSMFrameStackTooltip.index = TSMFrameStackTooltip.index + 1
end
elseif not self.altDown and not leftAltDown and rightAltDown then
self.altDown = "RIGHT"
if TSMFrameStackTooltip.index == 1 then
TSMFrameStackTooltip.index = TSMFrameStackTooltip.numFrames
else
TSMFrameStackTooltip.index = TSMFrameStackTooltip.index - 1
end
elseif self.altDown == "LEFT" and not leftAltDown then
self.altDown = nil
elseif self.altDown == "RIGHT" and not rightAltDown then
self.altDown = nil
end
for _, strata in ipairs(STRATA_ORDER) do
wipe(framesByStrata[strata])
end
local frame = EnumerateFrames()
while frame do
if frame ~= self.highlightFrame and not frame:IsForbidden() and frame:IsVisible() and MouseIsOver(frame) then
tinsert(framesByStrata[frame:GetFrameStrata()], frame)
for _, region in Vararg.Iterator(frame:GetRegions()) do
if region:IsObjectType("Texture") and not region:IsForbidden() and region:IsVisible() and MouseIsOver(region) and UIElements.GetByFrame(region) then
tinsert(framesByStrata[frame:GetFrameStrata()], region)
end
end
end
frame = EnumerateFrames(frame)
end
self:ClearLines()
self:AddDoubleLine("TSM Frame Stack", format("%0.2f, %0.2f", GetCursorPosition()))
local currentIndex = 1
local topFrame = nil
for _, strata in ipairs(STRATA_ORDER) do
if #framesByStrata[strata] > 0 then
sort(framesByStrata[strata], private.FrameLevelSortFunction)
self:AddLine(strata, 0.6, 0.6, 1)
for _, strataFrame in ipairs(framesByStrata[strata]) do
local isTexture = strataFrame:IsObjectType("Texture")
local level = (isTexture and strataFrame:GetParent() or strataFrame):GetFrameLevel()
local width = strataFrame:GetWidth()
local height = strataFrame:GetHeight()
local mouseEnabled = not isTexture and strataFrame:IsMouseEnabled()
local name = private.GetFrameName(strataFrame)
local isIndexedFrame = false
if not strmatch(name, "innerBorderFrame") then
if not topFrame and currentIndex == self.index then
topFrame = strataFrame
isIndexedFrame = true
end
currentIndex = currentIndex + 1
end
local text = format(" <%d%s> %s (%d, %d)", level, isTexture and "+" or "", name, Math.Round(width), Math.Round(height))
if isIndexedFrame then
self:AddLine(text, 0.9, 0.9, 0.5)
local element = UIElements.GetByFrame(strataFrame)
if element then
for _, k in ipairs(ELEMENT_STYLE_KEYS) do
local v = element[k]
if v ~= nil then
local vStr = private.GetStyleValueStr(v)
if vStr then
self:AddLine(format(" %s = %s", tostring(k), vStr), 0.7, 0.7, 0.7)
end
end
end
end
elseif mouseEnabled then
self:AddLine(text, 0.6, 1, 1)
else
self:AddLine(text, 0.9, 0.9, 0.9)
end
end
end
end
self.highlightFrame:ClearAllPoints()
self.highlightFrame:SetAllPoints(topFrame)
self.highlightFrame:SetFrameStrata("TOOLTIP")
self:Show()
end
function private.FrameLevelSortFunction(a, b)
local aLevel = a:IsObjectType("Texture") and (a:GetParent():GetFrameLevel() + 0.1) or a:GetFrameLevel()
local bLevel = b:IsObjectType("Texture") and (b:GetParent():GetFrameLevel() + 0.1) or b:GetFrameLevel()
return aLevel > bLevel
end
function private.TableValueSearch(tbl, searchValue, currentKey, visited)
visited = visited or {}
for key, value in pairs(tbl) do
if value == searchValue then
return (currentKey and (currentKey..".") or "")..key
elseif type(value) == "table" and (not value.__isa or value:__isa(TSM.UI.Element)) and not visited[value] then
visited[value] = true
local result = private.TableValueSearch(value, searchValue, (currentKey and (currentKey..".") or "")..key, visited)
if result then
return result
end
end
end
for _, key in ipairs(ELEMENT_ATTR_KEYS) do
local value = tbl[key]
if value == searchValue then
return (currentKey and (currentKey..".") or "")..key
elseif type(value) == "table" and (not value.__isa or value:__isa(TSM.UI.Element)) and not visited[value] then
visited[value] = true
local result = private.TableValueSearch(value, searchValue, (currentKey and (currentKey..".") or "")..key, visited)
if result then
return result
end
end
end
end
function private.GetFrameNodeInfo(frame)
local globalName = not frame:IsObjectType("Texture") and frame:GetName()
if globalName and not strmatch(globalName, "^TSM_UI_ELEMENT:") then
return globalName, frame:GetParent()
end
local parent = frame:GetParent()
local element = UIElements.GetByFrame(frame)
if element then
return element._id, parent
end
if parent then
-- check if this exists as an attribute of the parent table
local parentKey = Table.KeyByValue(parent, frame)
if parentKey then
return tostring(parentKey), parent
end
-- find the nearest element to which this frame belongs
local parentElement = nil
local testFrame = parent
while testFrame and not parentElement do
parentElement = UIElements.GetByFrame(testFrame)
testFrame = testFrame:GetParent()
end
if parentElement then
-- check if this exists as an attribute of this element
local tableKey = private.TableValueSearch(parentElement, frame)
if tableKey then
return tableKey, parentElement._frame
end
end
end
return nil, parent
end
function private.GetFrameName(frame)
local name, parent = private.GetFrameNodeInfo(frame)
local parentName = parent and (private.GetFrameName(parent)..".") or ""
name = name or gsub(tostring(frame), ": ?0*", ":")
return parentName..name
end
function private.GetStyleValueStr(value)
for key in pairs(COLOR_KEYS) do
if value == Theme.GetColor(key) then
return "ThemeColor<"..key..">"
end
end
for key in pairs(FEEDBACK_COLOR_KEYS) do
if value == Theme.GetFeedbackColor(key) then
return "ThemeColor<"..key..">"
end
end
if value == Theme.GetBlizzardColor() then
return "ThemeColor<BLIZZARD>"
end
for key in pairs(FONT_KEYS) do
if value == Theme.GetFont(key) then
return "ThemeFont<"..key..">"
end
end
if type(value) == "string" then
return "\""..value.."\""
elseif value ~= false then
return tostring(value)
end
return nil
end

243
Core/UI/MailingUI/Core.lua Normal file
View File

@@ -0,0 +1,243 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local MailingUI = TSM.UI:NewPackage("MailingUI")
local L = TSM.Include("Locale").GetTable()
local Delay = TSM.Include("Util.Delay")
local FSM = TSM.Include("Util.FSM")
local Event = TSM.Include("Util.Event")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
local private = {
settings = nil,
topLevelPages = {},
frame = nil,
fsm = nil,
defaultUISwitchBtn = nil,
isVisible = false,
}
local MIN_FRAME_SIZE = { width = 575, height = 400 }
-- ============================================================================
-- Module Functions
-- ============================================================================
function MailingUI.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "mailingUIContext", "showDefault")
:AddKey("global", "mailingUIContext", "frame")
private.FSMCreate()
end
function MailingUI.OnDisable()
-- hide the frame
private.fsm:ProcessEvent("EV_FRAME_HIDE")
end
function MailingUI.RegisterTopLevelPage(name, callback)
tinsert(private.topLevelPages, { name = name, callback = callback })
end
function MailingUI.IsVisible()
return private.isVisible
end
function MailingUI.SetSelectedTab(buttonText, redraw)
private.frame:SetSelectedNavButton(buttonText, redraw)
end
-- ============================================================================
-- Main Frame
-- ============================================================================
function private.CreateMainFrame()
TSM.UI.AnalyticsRecordPathChange("mailing")
-- Always show the Inbox first
private.settings.frame.page = 1
local frame = UIElements.New("LargeApplicationFrame", "base")
:SetParent(UIParent)
:SetSettingsContext(private.settings, "frame")
:SetMinResize(MIN_FRAME_SIZE.width, MIN_FRAME_SIZE.height)
:SetStrata("HIGH")
:AddSwitchButton(private.SwitchBtnOnClick)
:SetScript("OnHide", private.BaseFrameOnHide)
for _, info in ipairs(private.topLevelPages) do
frame:AddNavButton(info.name, info.callback)
end
private.frame = frame
return frame
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.BaseFrameOnHide()
TSM.UI.AnalyticsRecordClose("mailing")
private.fsm:ProcessEvent("EV_FRAME_HIDE")
end
function private.SwitchBtnOnClick(button)
private.settings.showDefault = button ~= private.defaultUISwitchBtn
private.fsm:ProcessEvent("EV_SWITCH_BTN_CLICKED")
end
function private.SwitchButtonOnEnter(button)
button:SetTextColor("TEXT")
:Draw()
end
function private.SwitchButtonOnLeave(button)
button:SetTextColor("TEXT_ALT")
:Draw()
end
-- ============================================================================
-- FSM
-- ============================================================================
function private.FSMCreate()
local function MailShowDelayed()
private.fsm:ProcessEvent("EV_MAIL_SHOW")
end
Event.Register("MAIL_SHOW", function()
Delay.AfterFrame("MAIL_SHOW_DELAYED", 0, MailShowDelayed)
end)
Event.Register("MAIL_CLOSED", function()
private.fsm:ProcessEvent("EV_MAIL_CLOSED")
end)
MailFrame:UnregisterEvent("MAIL_SHOW")
CancelEmote()
local fsmContext = {
frame = nil,
}
ScriptWrapper.Set(MailFrame, "OnHide", function()
private.fsm:ProcessEvent("EV_FRAME_HIDE")
end)
private.fsm = FSM.New("MAILING_UI")
:AddState(FSM.NewState("ST_CLOSED")
:AddTransition("ST_DEFAULT_OPEN")
:AddTransition("ST_FRAME_OPEN")
:AddEvent("EV_FRAME_TOGGLE", function(context)
assert(not private.settings.showDefault)
return "ST_FRAME_OPEN"
end)
:AddEvent("EV_MAIL_SHOW", function(context)
if private.settings.showDefault then
return "ST_DEFAULT_OPEN"
else
return "ST_FRAME_OPEN"
end
end)
)
:AddState(FSM.NewState("ST_DEFAULT_OPEN")
:SetOnEnter(function(context, isIgnored)
MailFrame_OnEvent(MailFrame, "MAIL_SHOW")
if not private.defaultUISwitchBtn then
private.defaultUISwitchBtn = UIElements.New("ActionButton", "switchBtn")
:SetSize(60, TSM.IsWowClassic() and 16 or 15)
:SetFont("BODY_BODY3")
:AddAnchor("TOPRIGHT", TSM.IsWowClassic() and -26 or -27, TSM.IsWowClassic() and -3 or -4)
:DisableClickCooldown()
:SetText(L["TSM4"])
:SetScript("OnClick", private.SwitchBtnOnClick)
:SetScript("OnEnter", private.SwitchButtonOnEnter)
:SetScript("OnLeave", private.SwitchButtonOnLeave)
private.defaultUISwitchBtn:_GetBaseFrame():SetParent(MailFrame)
end
if isIgnored then
private.defaultUISwitchBtn:Hide()
else
private.defaultUISwitchBtn:Show()
private.defaultUISwitchBtn:Draw()
end
end)
:AddTransition("ST_CLOSED")
:AddTransition("ST_FRAME_OPEN")
:AddEvent("EV_FRAME_HIDE", function(context)
OpenMailFrame:Hide()
CloseMail()
return "ST_CLOSED"
end)
:AddEventTransition("EV_MAIL_CLOSED", "ST_CLOSED")
:AddEvent("EV_SWITCH_BTN_CLICKED", function()
OpenMailFrame:Hide()
return "ST_FRAME_OPEN"
end)
)
:AddState(FSM.NewState("ST_FRAME_OPEN")
:SetOnEnter(function(context)
OpenAllBags()
CheckInbox()
DoEmote("READ", nil, true)
HideUIPanel(MailFrame)
assert(not context.frame)
context.frame = private.CreateMainFrame()
context.frame:Show()
context.frame:Draw()
private.isVisible = true
end)
:SetOnExit(function(context)
if context.frame then
context.frame:Hide()
context.frame:Release()
context.frame = nil
end
private.isVisible = false
end)
:AddTransition("ST_CLOSED")
:AddTransition("ST_DEFAULT_OPEN")
:AddEvent("EV_FRAME_HIDE", function(context)
CancelEmote()
CloseAllBags()
CloseMail()
return "ST_CLOSED"
end)
:AddEvent("EV_MAIL_SHOW", function(context)
OpenAllBags()
CheckInbox()
if not context.frame then
DoEmote("READ", nil, true)
context.frame = private.CreateMainFrame()
context.frame:Draw()
private.isVisible = true
end
end)
:AddEvent("EV_MAIL_CLOSED", function(context)
CancelEmote()
CloseAllBags()
return "ST_CLOSED"
end)
:AddEvent("EV_SWITCH_BTN_CLICKED", function()
return "ST_DEFAULT_OPEN"
end)
)
:Init("ST_CLOSED", fsmContext)
end

View File

@@ -0,0 +1,266 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Groups = TSM.UI.MailingUI:NewPackage("Groups")
local L = TSM.Include("Locale").GetTable()
local FSM = TSM.Include("Util.FSM")
local Log = TSM.Include("Util.Log")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
local private = {
settings = nil,
filterText = "",
fsm = nil
}
local SECONDS_PER_MINUTE = 60
-- ============================================================================
-- Module Functions
-- ============================================================================
function Groups.OnInitialize()
private.settings = Settings.NewView()
:AddKey("char", "mailingUIContext", "groupTree")
:AddKey("global", "mailingOptions", "resendDelay")
private.FSMCreate()
TSM.UI.MailingUI.RegisterTopLevelPage(L["Groups"], private.GetGroupsFrame)
end
-- ============================================================================
-- Groups UI
-- ============================================================================
function private.GetGroupsFrame()
TSM.UI.AnalyticsRecordPathChange("mailing", "groups")
return UIElements.New("Frame", "groups")
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Frame", "container")
:SetLayout("VERTICAL")
:SetPadding(8)
:SetBackgroundColor("PRIMARY_BG")
:AddChild(UIElements.New("Frame", "search")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Input", "input")
:SetIconTexture("iconPack.18x18/Search")
:SetClearButtonEnabled(true)
:AllowItemInsert(true)
:SetHintText(L["Search Groups"])
:SetValue(private.filterText)
:SetScript("OnValueChanged", private.GroupSearchOnValueChanged)
)
:AddChild(UIElements.New("Button", "expandAllBtn")
:SetSize(24, 24)
:SetMargin(8, 4, 0, 0)
:SetBackground("iconPack.18x18/Expand All")
:SetScript("OnClick", private.ExpandAllGroupsOnClick)
:SetTooltip(L["Expand / Collapse All Groups"])
)
:AddChild(UIElements.New("Button", "selectAllBtn")
:SetSize(24, 24)
:SetBackground("iconPack.18x18/Select All")
:SetScript("OnClick", private.SelectAllGroupsOnClick)
:SetTooltip(L["Select / Deselect All Groups"])
)
)
)
:AddChild(UIElements.New("Texture", "line")
:SetHeight(2)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("ApplicationGroupTree", "groupTree")
:SetMargin(0, 0, 0, 1)
:SetSettingsContext(private.settings, "groupTree")
:SetQuery(TSM.Groups.CreateQuery(), "Mailing")
:SetSearchString(private.filterText)
:SetScript("OnGroupSelectionChanged", private.GroupTreeOnGroupSelectionChanged)
)
:AddChild(UIElements.New("Frame", "footer")
:SetLayout("HORIZONTAL")
:SetHeight(26)
:SetBackgroundColor("PRIMARY_BG")
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "groupsText")
:SetFont("BODY_BODY2_MEDIUM")
:SetJustifyH("RIGHT")
:SetText(format(L["%d Groups Selected"], 0))
)
:AddChild(UIElements.New("Texture", "vline")
:SetWidth(1)
:SetMargin(8, 8, 2, 2)
:SetTexture("ACTIVE_BG_ALT")
)
:AddChild(UIElements.New("Text", "itemsText")
:SetWidth("AUTO")
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetJustifyH("RIGHT")
:SetText(L["Total Items"]..": ".."0")
)
)
:AddChild(UIElements.New("Texture", "line")
:SetHeight(2)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Frame", "bottom")
:SetLayout("VERTICAL")
:SetHeight(40)
:SetPadding(8)
:SetBackgroundColor("PRIMARY_BG_ALT")
:AddChild(UIElements.New("ActionButton", "mailGroupBtn")
:SetHeight(24)
:SetText(L["Mail Selected Groups"])
:SetScript("OnClick", private.MailBtnOnClick)
:SetModifierText(L["Mail Selected Groups (Dry Run)"], "CTRL")
:SetModifierText(L["Mail Selected Groups (Auto Resend)"], "SHIFT")
:SetModifierText(L["Mail Selected Groups (Dry Run + Auto Resend)"], "SHIFT", "CTRL")
:SetTooltip(format(L["Hold SHIFT to automatically resend items after '%s', and CTRL to perform a dry-run where no items are actually mailed, but chat messages will still display the result."], SecondsToTime(private.settings.resendDelay * SECONDS_PER_MINUTE)))
)
)
:SetScript("OnUpdate", private.FrameOnUpdate)
:SetScript("OnHide", private.FrameOnHide)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.FrameOnUpdate(frame)
frame:SetScript("OnUpdate", nil)
private.GroupTreeOnGroupSelectionChanged(frame:GetElement("groupTree"))
private.fsm:ProcessEvent("EV_FRAME_SHOW", frame)
end
function private.FrameOnHide(frame)
private.fsm:ProcessEvent("EV_FRAME_HIDE")
end
function private.GroupSearchOnValueChanged(input)
local text = strlower(input:GetValue())
if text == private.filterText then
return
end
private.filterText = text
input:GetElement("__parent.__parent.__parent.groupTree")
:SetSearchString(private.filterText)
:Draw()
end
function private.ExpandAllGroupsOnClick(button)
button:GetElement("__parent.__parent.__parent.groupTree")
:ToggleExpandAll()
end
function private.SelectAllGroupsOnClick(button)
button:GetElement("__parent.__parent.__parent.groupTree")
:ToggleSelectAll()
end
function private.GroupTreeOnGroupSelectionChanged(groupTree)
groupTree:GetElement("__parent.bottom.mailGroupBtn")
:SetDisabled(groupTree:IsSelectionCleared())
:Draw()
local numGroups, numItems = 0, 0
for _, groupPath in groupTree:SelectedGroupsIterator() do
numGroups = numGroups + 1
if groupPath == TSM.CONST.ROOT_GROUP_PATH then
-- TODO
else
for _ in TSM.Groups.ItemIterator(groupPath) do
numItems = numItems + 1
end
end
end
groupTree:GetElement("__parent.footer.groupsText")
:SetText(format(L["%d Groups Selected"], numGroups))
groupTree:GetElement("__parent.footer.itemsText")
:SetText(L["Total Items"]..": "..numItems)
groupTree:GetElement("__parent.footer")
:Draw()
end
function private.MailBtnOnClick(button)
private.fsm:ProcessEvent("EV_BUTTON_CLICKED", IsShiftKeyDown(), IsControlKeyDown())
end
-- ============================================================================
-- FSM
-- ============================================================================
function private.FSMCreate()
local fsmContext = {
frame = nil,
sending = false
}
local function UpdateButton(context)
context.frame:GetElement("bottom.mailGroupBtn")
:SetText(context.sending and L["Sending..."] or L["Mail Selected Groups"])
:SetPressed(context.sending)
:Draw()
end
private.fsm = FSM.New("MAILING_GROUPS")
:AddState(FSM.NewState("ST_HIDDEN")
:SetOnEnter(function(context)
TSM.Mailing.Send.KillThread()
TSM.Mailing.Groups.KillThread()
context.frame = nil
end)
:AddTransition("ST_SHOWN")
:AddTransition("ST_HIDDEN")
:AddEventTransition("EV_FRAME_SHOW", "ST_SHOWN")
)
:AddState(FSM.NewState("ST_SHOWN")
:SetOnEnter(function(context, frame)
if not context.frame then
context.frame = frame
end
UpdateButton(context)
end)
:AddTransition("ST_HIDDEN")
:AddTransition("ST_SENDING_START")
:AddEventTransition("EV_BUTTON_CLICKED", "ST_SENDING_START")
)
:AddState(FSM.NewState("ST_SENDING_START")
:SetOnEnter(function(context, sendRepeat, isDryRun)
context.sending = true
local groups = {}
for _, groupPath in context.frame:GetElement("groupTree"):SelectedGroupsIterator() do
tinsert(groups, groupPath)
end
if isDryRun then
Log.PrintUser(L["Performing a dry-run of your Mailing operations for the selected groups."])
end
TSM.Mailing.Groups.StartSending(private.FSMGroupsCallback, groups, sendRepeat, isDryRun)
UpdateButton(context)
end)
:SetOnExit(function(context)
context.sending = false
end)
:AddTransition("ST_SHOWN")
:AddTransition("ST_HIDDEN")
:AddEventTransition("EV_SENDING_DONE", "ST_SHOWN")
)
:AddDefaultEventTransition("EV_FRAME_HIDE", "ST_HIDDEN")
:Init("ST_HIDDEN", fsmContext)
end
function private.FSMGroupsCallback()
private.fsm:ProcessEvent("EV_SENDING_DONE")
end

1124
Core/UI/MailingUI/Inbox.lua Normal file

File diff suppressed because it is too large Load Diff

362
Core/UI/MailingUI/Other.lua Normal file
View File

@@ -0,0 +1,362 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Groups = TSM.UI.MailingUI:NewPackage("Other")
local L = TSM.Include("Locale").GetTable()
local FSM = TSM.Include("Util.FSM")
local Event = TSM.Include("Util.Event")
local Money = TSM.Include("Util.Money")
local String = TSM.Include("Util.String")
local ItemInfo = TSM.Include("Service.ItemInfo")
local BagTracking = TSM.Include("Service.BagTracking")
local PlayerInfo = TSM.Include("Service.PlayerInfo")
local UIElements = TSM.Include("UI.UIElements")
local private = {
frame = nil,
fsm = nil,
}
local PLAYER_NAME = UnitName("player")
local PLAYER_NAME_REALM = string.gsub(PLAYER_NAME.."-"..GetRealmName(), "%s+", "")
-- ============================================================================
-- Module Functions
-- ============================================================================
function Groups.OnInitialize()
private.FSMCreate()
TSM.UI.MailingUI.RegisterTopLevelPage(OTHER, private.GetOtherFrame)
end
-- ============================================================================
-- Other UI
-- ============================================================================
function private.GetOtherFrame()
TSM.UI.AnalyticsRecordPathChange("mailing", "other")
local frame = UIElements.New("Frame", "other")
:SetLayout("VERTICAL")
:SetPadding(10)
:SetBackgroundColor("PRIMARY_BG")
:AddChild(UIElements.New("Text", "enchant")
:SetHeight(24)
:SetFont("BODY_BODY1_BOLD")
:SetText(L["Mail Disenchantables"])
)
:AddChild(UIElements.New("Text", "enchantDesc")
:SetHeight(20)
:SetMargin(0, 0, 4, 0)
:SetFont("BODY_BODY2")
:SetText(L["Quickly mail all excess disenchantable items to a character"])
)
:AddChild(UIElements.New("Text", "enchantRecipient")
:SetHeight(15)
:SetMargin(0, 0, 12, 0)
:SetFont("BODY_BODY2")
:SetText(L["Recipient"])
)
:AddChild(UIElements.New("Frame", "enchantHeader")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(0, 0, 8, 0)
:AddChild(UIElements.New("Input", "recipient")
:SetMargin(0, 8, 0, 0)
:SetAutoComplete(PlayerInfo.GetConnectedAlts())
:SetClearButtonEnabled(true)
:SetValue(TSM.db.factionrealm.internalData.mailDisenchantablesChar)
:SetScript("OnValueChanged", private.EchantRecipientOnValueChanged)
)
:AddChild(UIElements.New("ActionButton", "send")
:SetSize(186, 24)
:SetDisabled(true)
:SetText(L["Send Disenchantables"])
:SetScript("OnClick", private.EnchantSendBtnOnClick)
)
)
:AddChild(UIElements.New("Text", "gold")
:SetHeight(24)
:SetMargin(0, 0, 24, 0)
:SetFont("BODY_BODY1_BOLD")
:SetText(L["Send Excess Gold to Banker"])
)
:AddChild(UIElements.New("Text", "goldDesc")
:SetHeight(18)
:SetMargin(0, 0, 4, 0)
:SetFont("BODY_BODY2")
:SetText(L["Quickly mail all excess gold (limited to a certain amount) to a character"])
)
:AddChild(UIElements.New("Frame", "goldTextHeader")
:SetLayout("HORIZONTAL")
:SetHeight(13)
:SetMargin(0, 0, 12, 0)
:AddChild(UIElements.New("Text", "recipient")
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY2")
:SetText(L["Recipient"])
)
:AddChild(UIElements.New("Text", "limit")
:SetWidth(128)
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY2")
:SetText(L["Limit"])
)
:AddChild(UIElements.New("Spacer", "spacer")
:SetWidth(106)
)
)
:AddChild(UIElements.New("Frame", "goldHeader")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(0, 0, 4, 0)
:AddChild(UIElements.New("Input", "recipient")
:SetMargin(0, 8, 0, 0)
:SetAutoComplete(PlayerInfo.GetConnectedAlts())
:SetClearButtonEnabled(true)
:SetValue(TSM.db.factionrealm.internalData.mailExcessGoldChar)
:SetScript("OnValueChanged", private.GoldRecipientOnValueChanged)
)
:AddChild(UIElements.New("Input", "limit")
:SetSize(128, 24)
:SetMargin(0, 8, 0, 0)
:SetBackgroundColor("PRIMARY_BG_ALT")
:SetJustifyH("RIGHT")
:SetValidateFunc(private.LimitValidateFunc)
:SetValue(Money.ToString(TSM.db.factionrealm.internalData.mailExcessGoldLimit, nil, "OPT_TRIM"))
:SetScript("OnValueChanged", private.LimitOnValueChanged)
)
:AddChild(UIElements.New("ActionButton", "send")
:SetSize(106, 24)
:SetDisabled(true)
:SetText(L["Send Gold"])
:SetScript("OnClick", private.GoldSendBtnOnClick)
)
)
:AddChild(UIElements.New("Spacer", "spacer")
)
:SetScript("OnUpdate", private.FrameOnUpdate)
:SetScript("OnHide", private.FrameOnHide)
private.frame = frame
return frame
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.FrameOnUpdate(frame)
frame:SetScript("OnUpdate", nil)
private.UpdateEnchantButton()
private.UpdateGoldButton()
private.fsm:ProcessEvent("EV_FRAME_SHOW", frame)
end
function private.FrameOnHide(frame)
private.fsm:ProcessEvent("EV_FRAME_HIDE")
end
function private.EchantRecipientOnValueChanged(input)
local value = input:GetValue()
if value == TSM.db.factionrealm.internalData.mailDisenchantablesChar then
return
end
TSM.db.factionrealm.internalData.mailDisenchantablesChar = value
private.UpdateEnchantButton()
end
function private.EnchantSendBtnOnClick(button)
local items = {}
local query = BagTracking.CreateQueryBags()
:OrderBy("slotId", true)
:Select("itemString", "quantity")
:Equal("isBoP", false)
:Equal("isBoA", false)
for _, itemString, quantity in query:Iterator() do
if ItemInfo.IsDisenchantable(itemString) and not ItemInfo.IsSoulbound(itemString) then
local quality = ItemInfo.GetQuality(itemString)
if quality <= TSM.db.global.mailingOptions.deMaxQuality then
items[itemString] = (items[itemString] or 0) + quantity
end
end
end
query:Release()
private.fsm:ProcessEvent("EV_BUTTON_CLICKED", TSM.db.factionrealm.internalData.mailDisenchantablesChar, 0, items)
end
function private.GoldRecipientOnValueChanged(input)
local value = input:GetValue()
if value == TSM.db.factionrealm.internalData.mailExcessGoldChar then
return
end
TSM.db.factionrealm.internalData.mailExcessGoldChar = value
private.UpdateGoldButton()
end
function private.GoldSendBtnOnClick(button)
local money = private.GetSendMoney()
private.fsm:ProcessEvent("EV_BUTTON_CLICKED", TSM.db.factionrealm.internalData.mailExcessGoldChar, money)
end
function private.ConvertLimitValue(value)
value = gsub(value, String.Escape(LARGE_NUMBER_SEPERATOR), "")
value = tonumber(value) or Money.FromString(value)
if not value then
return nil
end
return value > 0 and value <= MAXIMUM_BID_PRICE and value or nil
end
function private.LimitValidateFunc(_, value)
return private.ConvertLimitValue(value) and true or false
end
function private.LimitOnValueChanged(input)
local value = private.ConvertLimitValue(input:GetValue())
assert(value)
if value == TSM.db.factionrealm.internalData.mailExcessGoldLimit then
return
end
TSM.db.factionrealm.internalData.mailExcessGoldLimit = value
private.UpdateGoldButton()
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetSendMoney()
local money = GetMoney() - 30 - TSM.db.factionrealm.internalData.mailExcessGoldLimit
if money < 0 then
money = 0
end
return money
end
function private.UpdateEnchantButton()
local recipient = TSM.db.factionrealm.internalData.mailDisenchantablesChar
local enchantButton = private.frame:GetElement("enchantHeader.send")
if recipient == "" or recipient == PLAYER_NAME or recipient == PLAYER_NAME_REALM then
enchantButton:SetDisabled(true)
:Draw()
return
else
enchantButton:SetDisabled(false)
:Draw()
end
end
function private.UpdateGoldButton()
local recipient = TSM.db.factionrealm.internalData.mailExcessGoldChar
local goldButton = private.frame:GetElement("goldHeader.send")
if recipient == "" or recipient == PLAYER_NAME or recipient == PLAYER_NAME_REALM then
goldButton:SetDisabled(true)
:Draw()
return
end
if private.GetSendMoney() > 0 then
goldButton:SetDisabled(false)
else
goldButton:SetDisabled(true)
end
goldButton:Draw()
end
-- ============================================================================
-- FSM
-- ============================================================================
function private.FSMCreate()
Event.Register("PLAYER_MONEY", function()
private.fsm:ProcessEvent("EV_PLAYER_MONEY_UPDATE")
end)
local fsmContext = {
frame = nil,
sending = false
}
local function UpdateEnchant(context)
context.frame:GetElement("enchantHeader.send"):SetText(context.sending and L["Sending..."] or L["Send Disenchantables"])
:SetPressed(context.sending)
:Draw()
end
local function UpdateGold(context)
private.UpdateGoldButton()
context.frame:GetElement("goldHeader.send"):SetText(context.sending and L["Sending..."] or L["Send Gold"])
:SetPressed(context.sending)
:Draw()
end
private.fsm = FSM.New("MAILING_GROUPS")
:AddState(FSM.NewState("ST_HIDDEN")
:SetOnEnter(function(context)
context.frame = nil
end)
:AddTransition("ST_SHOWN")
:AddTransition("ST_HIDDEN")
:AddEventTransition("EV_FRAME_SHOW", "ST_SHOWN")
)
:AddState(FSM.NewState("ST_SHOWN")
:SetOnEnter(function(context, frame)
if not context.frame then
context.frame = frame
end
end)
:AddTransition("ST_HIDDEN")
:AddTransition("ST_SENDING_START")
:AddEvent("EV_PLAYER_MONEY_UPDATE", function(context)
UpdateGold(context)
end)
:AddEventTransition("EV_BUTTON_CLICKED", "ST_SENDING_START")
)
:AddState(FSM.NewState("ST_SENDING_START")
:SetOnEnter(function(context, recipient, money, items)
context.sending = true
if money > 0 then
TSM.Mailing.Send.StartSending(private.FSMOthersCallback, recipient, "TSM Mailing: Excess Gold", "", money)
UpdateGold(context)
elseif items then
TSM.Mailing.Send.StartSending(private.FSMOthersCallback, recipient, "TSM Mailing: Disenchantables", "", money, items)
UpdateEnchant(context)
end
end)
:SetOnExit(function(context)
context.sending = false
UpdateEnchant(context)
UpdateGold(context)
end)
:AddTransition("ST_SHOWN")
:AddTransition("ST_HIDDEN")
:AddEventTransition("EV_SENDING_DONE", "ST_SHOWN")
)
:AddDefaultEventTransition("EV_FRAME_HIDE", "ST_HIDDEN")
:Init("ST_HIDDEN", fsmContext)
end
function private.FSMOthersCallback()
private.fsm:ProcessEvent("EV_SENDING_DONE")
end

839
Core/UI/MailingUI/Send.lua Normal file
View File

@@ -0,0 +1,839 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Send = TSM.UI.MailingUI:NewPackage("Send")
local L = TSM.Include("Locale").GetTable()
local Delay = TSM.Include("Util.Delay")
local Money = TSM.Include("Util.Money")
local FSM = TSM.Include("Util.FSM")
local Database = TSM.Include("Util.Database")
local String = TSM.Include("Util.String")
local Event = TSM.Include("Util.Event")
local ItemString = TSM.Include("Util.ItemString")
local Theme = TSM.Include("Util.Theme")
local ItemInfo = TSM.Include("Service.ItemInfo")
local InventoryInfo = TSM.Include("Service.InventoryInfo")
local BagTracking = TSM.Include("Service.BagTracking")
local PlayerInfo = TSM.Include("Service.PlayerInfo")
local UIElements = TSM.Include("UI.UIElements")
local private = {
fsm = nil,
frame = nil,
db = nil,
query = nil,
recipient = "",
subject = "",
body = "",
money = 0,
isMoney = true,
isCOD = false,
}
local PLAYER_NAME = UnitName("player")
local PLAYER_NAME_REALM = gsub(PLAYER_NAME.."-"..GetRealmName(), "%s+", "")
local MAX_COD_AMOUNT = 10000 * COPPER_PER_GOLD
-- ============================================================================
-- Module Functions
-- ============================================================================
function Send.OnInitialize()
private.FSMCreate()
TSM.UI.MailingUI.RegisterTopLevelPage(L["Send"], private.GetSendFrame)
private.db = Database.NewSchema("MAILTRACKING_SEND_INFO")
:AddStringField("itemString")
:AddNumberField("quantity")
:Commit()
private.query = private.db:NewQuery()
end
function Send.SetSendRecipient(recipient)
private.recipient = recipient
end
-- ============================================================================
-- Send UI
-- ============================================================================
function private.GetSendFrame()
TSM.UI.AnalyticsRecordPathChange("mailing", "send")
local frame = UIElements.New("Frame", "send")
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Frame", "container")
:SetLayout("VERTICAL")
:SetBackgroundColor("PRIMARY_BG_ALT")
:AddChild(UIElements.New("Text", "recipient")
:SetMargin(8, 8, 8, 8)
:SetHeight(24)
:SetFont("BODY_BODY1_BOLD")
:SetText(L["Recipient"])
)
:AddChild(UIElements.New("Frame", "name")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(8, 8, 0, 8)
:AddChild(UIElements.New("Input", "input")
:SetHintText(L["Enter recipient name"])
:SetAutoComplete(PlayerInfo.GetConnectedAlts())
:SetClearButtonEnabled(true)
:SetValue(private.recipient)
:SetScript("OnValueChanged", private.RecipientOnValueChanged)
)
:AddChild(UIElements.New("ActionButton", "contacts")
:SetWidth(152)
:SetMargin(8, 0, 0, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Contacts"])
:SetScript("OnClick", private.ContactsBtnOnClick)
)
)
:AddChild(UIElements.New("Frame", "subject")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(8, 8, 0, 16)
:AddChild(UIElements.New("Button", "icon")
:SetMargin(4, 4, 0, 0)
:SetBackgroundAndSize("iconPack.12x12/Add/Circle")
:SetScript("OnClick", private.SubjectBtnOnClick)
)
:AddChild(UIElements.New("Button", "text")
:SetWidth("AUTO")
:SetText(L["Add subject & description (optional)"])
:SetFont("BODY_BODY2")
:SetScript("OnClick", private.SubjectBtnOnClick)
)
:AddChild(UIElements.New("Button", "button")
:SetWidth("AUTO")
:SetMargin(8, 0, 0, 0)
:SetFont("BODY_BODY2")
:SetTextColor("INDICATOR")
:SetText(L["Edit"])
:SetScript("OnClick", private.SubjectBtnOnClick)
)
:AddChild(UIElements.New("Spacer", "spacer"))
)
:AddChild(UIElements.New("Frame", "header")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(8, 8, 0, 4)
:AddChild(UIElements.New("Text", "items")
:SetFont("BODY_BODY1_BOLD")
:SetText(L["Select Items to Attach"])
)
)
:AddChild(UIElements.New("Frame", "dragBox")
:SetLayout("VERTICAL")
:SetBackgroundColor("PRIMARY_BG")
:RegisterForDrag("LeftButton")
:SetScript("OnReceiveDrag", private.DragBoxOnItemRecieve)
:SetScript("OnMouseUp", private.DragBoxOnItemRecieve)
:AddChild(UIElements.New("QueryScrollingTable", "items")
:GetScrollingTableInfo()
:NewColumn("item")
:SetTitle(L["Items"])
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetIconSize(12)
:SetTextInfo("itemString", TSM.UI.GetColoredItemName)
:SetIconInfo("itemString", ItemInfo.GetTexture)
:SetTooltipInfo("itemString")
:SetTooltipLinkingDisabled(true)
:DisableHiding()
:Commit()
:NewColumn("quantity")
:SetTitle(L["Amount"])
:SetWidth(60)
:SetFont("TABLE_TABLE1")
:SetJustifyH("LEFT")
:SetTextInfo("quantity")
:DisableHiding()
:Commit()
:Commit()
:SetQuery(private.query)
:SetScript("OnRowClick", private.QueryOnRowClick)
:SetScript("OnDataUpdated", private.SendOnDataUpdated)
)
:AddChild(UIElements.New("Texture", "line")
:SetHeight(2)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Frame", "footer")
:SetLayout("HORIZONTAL")
:SetHeight(26)
:SetPadding(8, 8, 3, 3)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "items")
:SetWidth(144)
:SetFont("BODY_BODY2_MEDIUM")
:SetJustifyH("RIGHT")
:Hide()
)
:AddChild(UIElements.New("Texture", "vline")
:SetWidth(1)
:SetMargin(8, 8, 3, 3)
:SetTexture("ACTIVE_BG_ALT")
:Hide()
)
:AddChild(UIElements.New("Text", "postage")
:SetWidth(150)
:SetFont("BODY_BODY2_MEDIUM")
:SetJustifyH("RIGHT")
:SetText(L["Total Postage"]..": "..Money.ToString(30))
)
)
)
:AddChild(UIElements.New("Texture", "line")
:SetHeight(2)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Frame", "check")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(8, 0, 8, 6)
:AddChild(UIElements.New("Checkbox", "sendCheck")
:SetWidth("AUTO")
:SetMargin(0, 0, 1, 0)
:SetFont("BODY_BODY2")
:SetCheckboxPosition("LEFT")
:SetChecked(private.isMoney)
:SetText(L["Send Money"])
:SetScript("OnValueChanged", private.SendOnValueChanged)
)
:AddChild(UIElements.New("Spacer", "spacer"))
)
:AddChild(UIElements.New("Frame", "checkbox")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(8, 8, 0, 8)
:AddChild(UIElements.New("Checkbox", "cod")
:SetSize("AUTO", 20)
:SetFont("BODY_BODY2")
:SetCheckboxPosition("LEFT")
:SetChecked(private.isCOD)
:SetText(L["Make Cash On Delivery?"])
:SetDisabled(true)
:SetScript("OnValueChanged", private.CODOnValueChanged)
)
:AddChild(UIElements.New("Text", "amountText")
:SetHeight(20)
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetJustifyH("RIGHT")
:SetText(L["Amount"]..":")
)
:AddChild(UIElements.New("Input", "moneyInput")
:SetWidth(160)
:SetBackgroundColor("ACTIVE_BG")
:SetFont("BODY_BODY2_MEDIUM")
:SetValidateFunc(private.MoneyValidateFunc)
:SetJustifyH("RIGHT")
:SetValue(Money.ToString(private.money))
:SetScript("OnValueChanged", private.MoneyOnValueChanged)
)
)
)
:AddChild(UIElements.New("Texture", "line")
:SetHeight(2)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Frame", "footer")
:SetLayout("HORIZONTAL")
:SetHeight(40)
:SetPadding(8)
:SetBackgroundColor("PRIMARY_BG_ALT")
:AddChild(UIElements.New("ActionButton", "sendMail")
:SetHeight(24)
:SetText(L["Send Mail"])
:SetScript("OnClick", private.SendMail)
:SetDisabled(private.recipient == "")
)
:AddChild(UIElements.New("Button", "clear")
:SetWidth("AUTO")
:SetHeight(24)
:SetMargin(16, 10, 0, 0)
:SetFont("BODY_BODY3_MEDIUM")
:SetJustifyH("LEFT")
:SetText(L["Clear All"])
:SetScript("OnClick", private.ClearOnClick)
)
)
:SetScript("OnUpdate", private.SendFrameOnUpdate)
:SetScript("OnHide", private.SendFrameOnHide)
private.frame = frame
return frame
end
function private.SubjectBtnOnClick(button)
button:GetBaseElement():ShowDialogFrame(UIElements.New("Frame", "frame")
:SetLayout("VERTICAL")
:SetSize(478, 314)
:SetPadding(12)
:AddAnchor("CENTER")
:SetBackgroundColor("FRAME_BG", true)
:AddChild(UIElements.New("Frame", "header")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(0, 0, -4, 14)
:AddChild(UIElements.New("Spacer", "spacer")
:SetWidth(20)
)
:AddChild(UIElements.New("Text", "title")
:SetFont("BODY_BODY1_BOLD")
:SetJustifyH("CENTER")
:SetText(L["Add Subject / Description"])
)
:AddChild(UIElements.New("Button", "closeBtn")
:SetMargin(0, -4, 0, 0)
:SetBackgroundAndSize("iconPack.24x24/Close/Default")
:SetScript("OnClick", private.CloseDialog)
)
)
:AddChild(UIElements.New("Text", "subjectText")
:SetMargin(0, 0, 0, 4)
:SetHeight(20)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Subject"])
)
:AddChild(UIElements.New("Input", "subjectInput")
:SetHeight(24)
:SetMargin(0, 0, 0, 8)
:SetBackgroundColor("PRIMARY_BG_ALT")
:SetMaxLetters(64)
:SetClearButtonEnabled(true)
:SetValue(private.subject)
:SetTabPaths("__parent.descriptionInput", "__parent.descriptionInput")
:SetScript("OnValueChanged", private.SubjectOnValueChanged)
)
:AddChild(UIElements.New("Text", "descriptionText")
:SetHeight(20)
:SetMargin(0, 0, 0, 4)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(DESCRIPTION)
)
:AddChild(UIElements.New("MultiLineInput", "descriptionInput")
:SetBackgroundColor("PRIMARY_BG_ALT")
:SetIgnoreEnter()
:SetMaxLetters(500)
:SetValue(private.body)
:SetTabPaths("__parent.subjectInput", "__parent.subjectInput")
:SetScript("OnValueChanged", private.DesciptionOnValueChanged)
)
:AddChild(UIElements.New("Frame", "footer")
:SetLayout("HORIZONTAL")
:SetMargin(0, 0, 4, 12)
:AddChild(UIElements.New("Text", "title")
:SetHeight(20)
:SetMargin(0, 4, 0, 0)
:SetFont("BODY_BODY3")
:SetJustifyH("RIGHT")
:SetText(format(L["(%d/500 Characters)"], #private.body))
)
:AddChild(UIElements.New("Button", "clearAll")
:SetSize("AUTO", 20)
:SetFont("BODY_BODY3_MEDIUM")
:SetJustifyH("LEFT")
:SetText(L["Clear All"])
:SetDisabled(private.subject == "" and private.body == "")
:SetScript("OnClick", private.SubjectClearAllBtnOnClick)
)
)
:AddChild(UIElements.New("ActionButton", "addMailBtn")
:SetHeight(24)
:SetText(L["Add to Mail"])
:SetScript("OnClick", private.CloseDialog)
:SetDisabled(private.subject == "" and private.body == "")
)
:SetScript("OnHide", private.DialogOnHide)
)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.SendFrameOnUpdate(frame)
frame:SetScript("OnUpdate", nil)
private.fsm:ProcessEvent("EV_FRAME_SHOW", frame)
end
function private.SendFrameOnHide(frame)
assert(frame == private.frame)
private.frame = nil
private.fsm:ProcessEvent("EV_FRAME_HIDE")
end
function private.ClearOnClick(button)
private.fsm:ProcessEvent("EV_MAIL_CLEAR", true)
end
function private.CloseDialog(button)
button:GetBaseElement():HideDialog()
private.fsm:ProcessEvent("EV_DIALOG_HIDDEN")
end
function private.DialogOnHide(button)
private.fsm:ProcessEvent("EV_DIALOG_HIDDEN")
end
function private.ContactsBtnOnClick(button)
TSM.UI.Util.Contacts.ShowDialog(button, button:GetElement("__parent.input"), private.RecipientOnValueChanged)
end
function private.DragBoxOnItemRecieve(frame, button)
if not CursorHasItem() then
ClearCursor()
return
end
if private.query:Count() >= 12 then
ClearCursor()
UIErrorsFrame:AddMessage(ERR_MAIL_INVALID_ATTACHMENT_SLOT, 1.0, 0.1, 0.1, 1.0)
return
end
local _, _, subType = GetCursorInfo()
local itemString = ItemString.Get(subType)
local stackSize = nil
local query = BagTracking.CreateQueryBags()
:OrderBy("slotId", true)
:Select("bag", "slot", "quantity")
:Equal("isBoP", false)
:Equal("itemString", itemString)
for _, bag, slot, quantity in query:Iterator() do
if InventoryInfo.IsBagSlotLocked(bag, slot) then
stackSize = quantity
end
end
query:Release()
ClearCursor()
if not stackSize then
return
end
private.DatabaseNewRow(itemString, stackSize)
end
function private.QueryOnRowClick(scrollingTable, row, button)
if button == "RightButton" then
private.db:DeleteRow(row)
end
end
function private.SendOnDataUpdated()
private.fsm:ProcessEvent("EV_MAIL_DATA_UPDATED")
end
function private.SubjectClearAllBtnOnClick(button)
private.subject = ""
private.body = ""
button:GetElement("__parent.__parent.subjectInput")
:SetFocused(false)
:SetValue(private.subject)
:Draw()
button:GetElement("__parent.__parent.descriptionInput")
:SetFocused(false)
:SetValue(private.body)
:Draw()
button:GetElement("__parent.title")
:SetText(format(L["(%d/500 Characters)"], 0))
:Draw()
button:SetDisabled(true)
:Draw()
button:GetElement("__parent.__parent.addMailBtn")
:SetDisabled(true)
:Draw()
end
function private.RecipientOnValueChanged(input)
local value = input:GetValue()
if value == private.recipient then
return
end
private.recipient = value
private.UpdateSendFrame()
end
function private.SubjectOnValueChanged(input)
local value = input:GetValue()
if value == private.subject then
return
end
private.subject = value
input:GetElement("__parent.footer.clearAll")
:SetDisabled(private.subject == "" and private.body == "")
:Draw()
input:GetElement("__parent.addMailBtn")
:SetDisabled(private.subject == "" and private.body == "")
:Draw()
end
function private.DesciptionOnValueChanged(input)
local text = input:GetValue()
if text == private.body then
return
end
private.body = text
input:GetElement("__parent.footer.title")
:SetText(format(L["(%d/500 Characters)"], #private.body))
:Draw()
input:GetElement("__parent.footer.clearAll")
:SetDisabled(private.subject == "" and private.body == "")
:Draw()
input:GetElement("__parent.addMailBtn")
:SetDisabled(private.subject == "" and private.body == "")
:Draw()
end
function private.SendOnValueChanged(checkbox)
if checkbox:IsChecked() then
checkbox:GetElement("__parent.__parent.checkbox.cod"):SetChecked(false)
:Draw()
private.isMoney = true
private.isCOD = false
else
private.isMoney = false
end
end
function private.CODOnValueChanged(checkbox)
if checkbox:IsChecked() then
checkbox:GetElement("__parent.__parent.check.sendCheck"):SetChecked(false)
:Draw()
local input = checkbox:GetElement("__parent.moneyInput")
local value = private.ConvertMoneyValue(input:GetValue())
private.money = private.isCOD and min(value, MAX_COD_AMOUNT) or value
input:SetValue(Money.ToString(private.money))
:Draw()
private.isMoney = false
private.isCOD = true
else
private.isCOD = false
end
end
function private.ConvertMoneyValue(value)
value = gsub(value, String.Escape(LARGE_NUMBER_SEPERATOR), "")
value = tonumber(value) or Money.FromString(value)
if not value then
return nil
end
local maxVal = private.isCOD and MAX_COD_AMOUNT or MAXIMUM_BID_PRICE
return value >= 0 and value <= maxVal and value or nil
end
function private.MoneyValidateFunc(_, value)
return private.ConvertMoneyValue(value) and true or false
end
function private.MoneyOnValueChanged(input)
local value = private.ConvertMoneyValue(input:GetValue())
assert(value)
if value == private.money then
return
end
private.money = value
input:SetValue(Money.ToString(value))
:Draw()
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.SendMail(button)
local money = 0
if private.money > 0 and private.isMoney then
money = private.money
elseif private.money > 0 and private.isCOD then
money = private.money * -1
end
button:GetElement("__parent.__parent.container.name.input"):SetFocused(false)
private.UpdateRecentlyMailed(private.recipient)
if private.query:Count() > 0 then
local items = {}
for _, row in private.query:Iterator() do
local itemString = row:GetField("itemString")
local quantity = row:GetField("quantity")
if items[itemString] then
items[itemString] = items[itemString] + quantity
else
items[itemString] = quantity
end
end
private.fsm:ProcessEvent("EV_BUTTON_CLICKED", IsShiftKeyDown(), private.recipient, private.subject, private.body, money, items)
else
private.fsm:ProcessEvent("EV_BUTTON_CLICKED", IsShiftKeyDown(), private.recipient, private.subject, private.body, money)
end
end
function private.UpdateRecentlyMailed(recipient)
if recipient == UnitName("player") or recipient == PLAYER_NAME_REALM then
return
end
local size = 0
local oldestName = nil
local oldestTime = nil
for k, v in pairs(TSM.db.global.mailingOptions.recentlyMailedList) do
size = size + 1
if not oldestName or not oldestTime or oldestTime > v then
oldestName = k
oldestTime = v
end
end
if size >= 20 then
TSM.db.global.mailingOptions.recentlyMailedList[oldestName] = nil
end
TSM.db.global.mailingOptions.recentlyMailedList[recipient] = time()
end
function private.UpdateSendFrame()
if not private.frame then
return
end
local sendMail = private.frame:GetElement("footer.sendMail")
if private.recipient ~= "" then
sendMail:SetDisabled(false)
else
sendMail:SetDisabled(true)
end
sendMail:Draw()
end
function private.DatabaseNewRow(itemString, stackSize)
private.db:NewRow()
:SetField("itemString", itemString)
:SetField("quantity", stackSize)
:Create()
end
-- ============================================================================
-- FSM
-- ============================================================================
function private.FSMCreate()
Event.Register("MAIL_CLOSED", function()
private.fsm:ProcessEvent("EV_MAIL_CLEAR")
end)
Event.Register("MAIL_SEND_INFO_UPDATE", function()
private.fsm:ProcessEvent("EV_MAIL_INFO_UPDATE")
end)
Event.Register("MAIL_FAILED", function()
private.fsm:ProcessEvent("EV_SENDING_DONE")
end)
local fsmContext = {
frame = nil,
sending = false,
keepInfo = false
}
local function UpdateFrame(context)
if not context.frame then
return
end
local subject = context.frame:GetElement("container.subject")
if private.subject == "" and private.body == "" then
subject:GetElement("icon")
:SetBackgroundAndSize("iconPack.12x12/Add/Circle")
:Draw()
subject:GetElement("text")
:SetText(L["Add subject & description (optional)"])
:Draw()
subject:GetElement("button")
:Hide()
else
subject:GetElement("icon")
:SetBackgroundAndSize("iconPack.12x12/Checkmark/Default")
:Draw()
subject:GetElement("text")
:SetText(L["Subject & Description added"])
:Draw()
subject:GetElement("button")
:Show()
end
subject:Draw()
local items = context.frame:GetElement("container.dragBox.footer.items")
local line = context.frame:GetElement("container.dragBox.footer.vline")
local postage = context.frame:GetElement("container.dragBox.footer.postage")
local send = context.frame:GetElement("container.check.sendCheck")
local cod = context.frame:GetElement("container.checkbox.cod")
local size = private.query:Count()
if size > 0 then
postage:SetText(L["Total Postage"]..": "..Money.ToString(30 * size))
:Draw()
items:SetText(format(L["%s Items Selected"], Theme.GetFeedbackColor("GREEN"):ColorText(size.."/"..ATTACHMENTS_MAX_SEND)))
:Show()
:Draw()
line:Show()
cod:SetDisabled(false)
:Draw()
else
postage:SetText(L["Total Postage"]..": "..Money.ToString(30))
:Draw()
items:Hide()
line:Hide()
cod:SetDisabled(true)
:Draw()
send:SetChecked(true)
:SetDisabled(false)
:Draw()
end
end
local function UpdateButton(context)
context.frame:GetElement("footer.sendMail")
:SetText(context.sending and L["Sending..."] or L["Send Mail"])
:SetPressed(context.sending)
:Draw()
end
local function UpdateSendMailInfo(context)
if private.query:Count() >= 12 then
UIErrorsFrame:AddMessage(ERR_MAIL_INVALID_ATTACHMENT_SLOT, 1.0, 0.1, 0.1, 1.0)
else
for i = 1, ATTACHMENTS_MAX_SEND do
local itemName, _, _, stackCount = GetSendMailItem(i)
if itemName and stackCount then
local itemLink = GetSendMailItemLink(i)
local itemString = ItemString.Get(itemLink)
private.DatabaseNewRow(itemString, stackCount)
break
end
end
end
ClearSendMail()
end
local function ClearMail(context, keepInfo, redraw)
if not keepInfo then
private.recipient = ""
end
private.subject = ""
private.body = ""
private.money = 0
private.isMoney = true
private.isCOD = false
private.db:Truncate()
if redraw and context.frame then
context.frame:GetElement("container.name.input")
:SetValue(private.recipient)
:Draw()
context.frame:GetElement("container.checkbox.moneyInput")
:SetValue(Money.ToString(private.money))
:Draw()
if not keepInfo then
context.frame:GetElement("footer.sendMail")
:SetDisabled(true)
:Draw()
end
end
UpdateFrame(context)
end
local function SendMailShowing()
SetSendMailShowing(true)
end
private.fsm = FSM.New("MAILING_SEND")
:AddState(FSM.NewState("ST_HIDDEN")
:SetOnEnter(function(context)
TSM.Mailing.Send.KillThread()
SetSendMailShowing(false)
context.frame = nil
end)
:AddTransition("ST_SHOWN")
:AddTransition("ST_HIDDEN")
:AddEventTransition("EV_FRAME_SHOW", "ST_SHOWN")
)
:AddState(FSM.NewState("ST_SHOWN")
:SetOnEnter(function(context, frame)
if not context.frame then
context.frame = frame
UpdateFrame(context)
Delay.AfterFrame("setMailShowing", 2, SendMailShowing)
end
UpdateButton(context)
end)
:AddTransition("ST_HIDDEN")
:AddTransition("ST_SENDING_START")
:AddEvent("EV_DIALOG_HIDDEN", function(context)
UpdateFrame(context)
end)
:AddEvent("EV_MAIL_INFO_UPDATE", function(context)
UpdateSendMailInfo(context)
UpdateFrame(context)
end)
:AddEvent("EV_MAIL_DATA_UPDATED", function(context)
UpdateFrame(context)
end)
:AddEvent("EV_MAIL_CLEAR", function(context, redraw)
ClearMail(context, IsShiftKeyDown(), redraw)
end)
:AddEventTransition("EV_BUTTON_CLICKED", "ST_SENDING_START")
)
:AddState(FSM.NewState("ST_SENDING_START")
:SetOnEnter(function(context, keepInfo, recipient, subject, body, money, items)
context.sending = true
context.keepInfo = keepInfo
private.db:SetQueryUpdatesPaused(true)
TSM.Mailing.Send.StartSending(private.FSMSendCallback, recipient, subject, body, money, items)
UpdateButton(context)
end)
:SetOnExit(function(context)
context.sending = false
private.db:SetQueryUpdatesPaused(false)
ClearMail(context, context.keepInfo, true)
UpdateFrame(context)
end)
:AddTransition("ST_SHOWN")
:AddTransition("ST_HIDDEN")
:AddEventTransition("EV_SENDING_DONE", "ST_SHOWN")
)
:AddDefaultEvent("EV_FRAME_HIDE", function(context)
context.frame = nil
return "ST_HIDDEN"
end)
:Init("ST_HIDDEN", fsmContext)
end
function private.FSMSendCallback()
private.fsm:ProcessEvent("EV_SENDING_DONE")
end

91
Core/UI/MainUI/Core.lua Normal file
View File

@@ -0,0 +1,91 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local MainUI = TSM:NewPackage("MainUI")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
local private = {
settings = nil,
topLevelPages = {},
frame = nil,
}
local MIN_FRAME_SIZE = { width = 720, height = 588 }
-- ============================================================================
-- Module Functions
-- ============================================================================
function MainUI.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "mainUIContext", "frame")
end
function MainUI.OnDisable()
-- hide the frame
if private.frame then
MainUI.Toggle()
end
end
function MainUI.RegisterTopLevelPage(name, callback)
tinsert(private.topLevelPages, { name = name, callback = callback })
end
function MainUI.Toggle()
if private.frame then
-- it's already shown, so hide it
private.frame:Hide()
assert(not private.frame)
else
private.frame = private.CreateMainFrame()
private.frame:Draw()
private.frame:Show()
end
end
-- ============================================================================
-- Main Frame
-- ============================================================================
function private.CreateMainFrame()
TSM.UI.AnalyticsRecordPathChange("main")
-- Always show the Dashboard first
private.settings.frame.page = 1
local frame = UIElements.New("LargeApplicationFrame", "base")
:SetParent(UIParent)
:SetSettingsContext(private.settings, "frame")
:SetMinResize(MIN_FRAME_SIZE.width, MIN_FRAME_SIZE.height)
:SetStrata("HIGH")
:AddPlayerGold()
:AddAppStatusIcon()
:SetScript("OnHide", private.BaseFrameOnHide)
for _, info in ipairs(private.topLevelPages) do
frame:AddNavButton(info.name, info.callback)
end
local whatsNewDialog = TSM.UI.WhatsNew.GetDialog()
if whatsNewDialog then
frame:ShowDialogFrame(whatsNewDialog)
end
return frame
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.BaseFrameOnHide(frame)
assert(frame == private.frame)
frame:Release()
private.frame = nil
TSM.UI.AnalyticsRecordClose("main")
end

View File

@@ -0,0 +1,936 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Dashboard = TSM.MainUI:NewPackage("Dashboard")
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
local Money = TSM.Include("Util.Money")
local Math = TSM.Include("Util.Math")
local Theme = TSM.Include("Util.Theme")
local Analytics = TSM.Include("Util.Analytics")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
local private = {
settings = nil,
characterGuilds = {},
tempTimeTable = {},
selectedTimeRange = nil,
}
local SECONDS_PER_DAY = 60 * 60 * 24
local MIN_GRAPH_STEP_SIZE = TSM.IsWowClassic() and COPPER_PER_GOLD or (COPPER_PER_GOLD * 1000)
local TIME_RANGE_LOOKUP = {
["1d"] = SECONDS_PER_DAY,
["1w"] = SECONDS_PER_DAY * 7,
["1m"] = SECONDS_PER_DAY * 30,
["3m"] = SECONDS_PER_DAY * 91,
["6m"] = SECONDS_PER_DAY * 183,
["1y"] = SECONDS_PER_DAY * 365,
["2y"] = SECONDS_PER_DAY * 730,
["all"] = -1,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Dashboard.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "mainUIContext", "dashboardDividedContainer")
:AddKey("global", "mainUIContext", "dashboardUnselectedCharacters")
:AddKey("global", "mainUIContext", "dashboardTimeRange")
private.selectedTimeRange = private.settings.dashboardTimeRange
TSM.MainUI.RegisterTopLevelPage(L["Dashboard"], private.GetDashboardFrame)
end
-- ============================================================================
-- Dashboard UI
-- ============================================================================
function private.GetDashboardFrame()
TSM.UI.AnalyticsRecordPathChange("main", "dashboard")
private.selectedTimeRange = private.settings.dashboardTimeRange
wipe(private.characterGuilds)
local prevUnselectedCharacters = TempTable.Acquire()
for characterGuild in pairs(private.settings.dashboardUnselectedCharacters) do
prevUnselectedCharacters[characterGuild] = true
end
wipe(private.settings.dashboardUnselectedCharacters)
for characterGuild in TSM.Accounting.GoldTracker.CharacterGuildIterator() do
tinsert(private.characterGuilds, characterGuild)
private.settings.dashboardUnselectedCharacters[characterGuild] = prevUnselectedCharacters[characterGuild] or nil
end
TempTable.Release(prevUnselectedCharacters)
local frame = UIElements.New("DividedContainer", "dashboard")
:SetSettingsContext(private.settings, "dashboardDividedContainer")
:SetMinWidth(200, 407)
:SetBackgroundColor("PRIMARY_BG")
:SetLeftChild(UIElements.New("Frame", "news")
:SetLayout("VERTICAL")
:SetBackgroundColor("PRIMARY_BG")
:AddChild(UIElements.New("Text", "text")
:SetSize("AUTO", 24)
:SetMargin(8)
:SetFont("BODY_BODY1_BOLD")
:SetText(L["News & Information"])
)
:AddChild(UIElements.New("ScrollFrame", "content")
:SetPadding(8, 8, 0, 0)
)
)
:SetRightChild(UIElements.New("Frame", "content")
:SetLayout("VERTICAL")
:SetPadding(8)
:SetBackgroundColor("PRIMARY_BG")
:SetScript("OnUpdate", private.ContentOnUpdate)
:AddChild(UIElements.New("Frame", "goldHeader")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Text", "text")
:SetWidth("AUTO")
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY1_BOLD")
:SetText(L["Player Gold"])
)
:AddChild(UIElements.New("MultiselectionDropdown", "playerDropdown")
:SetSize(157, 22)
:SetItems(private.characterGuilds, private.characterGuilds)
:SetUnselectedItemKeys(private.settings.dashboardUnselectedCharacters)
:SetSelectionText(L["No Players"], L["%d Players"], L["All Players"])
:SetScript("OnSelectionChanged", private.DropdownOnSelectionChanged)
)
:AddChild(UIElements.New("Spacer"))
:AddChild(UIElements.New("Text", "hoverTime")
:SetWidth("AUTO")
:SetFont("BODY_BODY3_MEDIUM")
:SetText("")
)
:AddChild(UIElements.New("Frame", "timeBtns")
:SetLayout("HORIZONTAL")
:AddChild(UIElements.New("Button", "1d")
:SetMargin(8, 0, 0, 0)
:SetSize(20, 20)
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetContext(TIME_RANGE_LOOKUP["1d"])
:SetText(L["1D"])
:SetScript("OnClick", private.TimeBtnOnClick)
)
:AddChild(UIElements.New("Button", "1w")
:SetMargin(8, 0, 0, 0)
:SetSize(20, 20)
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetContext(TIME_RANGE_LOOKUP["1w"])
:SetText(L["1W"])
:SetScript("OnClick", private.TimeBtnOnClick)
)
:AddChild(UIElements.New("Button", "1m")
:SetMargin(8, 0, 0, 0)
:SetSize(20, 20)
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetContext(TIME_RANGE_LOOKUP["1m"])
:SetText(L["1M"])
:SetScript("OnClick", private.TimeBtnOnClick)
)
:AddChild(UIElements.New("Button", "3m")
:SetMargin(8, 0, 0, 0)
:SetSize(20, 20)
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetContext(TIME_RANGE_LOOKUP["3m"])
:SetText(L["3M"])
:SetScript("OnClick", private.TimeBtnOnClick)
)
:AddChild(UIElements.New("Button", "6m")
:SetMargin(8, 0, 0, 0)
:SetSize(20, 20)
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetContext(TIME_RANGE_LOOKUP["6m"])
:SetText(L["6M"])
:SetScript("OnClick", private.TimeBtnOnClick)
)
:AddChild(UIElements.New("Button", "1y")
:SetMargin(8, 0, 0, 0)
:SetSize(20, 20)
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetContext(TIME_RANGE_LOOKUP["1y"])
:SetText(L["1Y"])
:SetScript("OnClick", private.TimeBtnOnClick)
)
:AddChild(UIElements.New("Button", "2y")
:SetMargin(8, 0, 0, 0)
:SetSize(20, 20)
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetContext(TIME_RANGE_LOOKUP["2y"])
:SetText(L["2Y"])
:SetScript("OnClick", private.TimeBtnOnClick)
)
:AddChild(UIElements.New("Button", "all")
:SetMargin(8, 0, 0, 0)
:SetSize(20, 20)
:SetFont("TABLE_TABLE1")
:SetTextColor("TEXT")
:SetContext(TIME_RANGE_LOOKUP["all"])
:SetText(ALL)
:SetScript("OnClick", private.TimeBtnOnClick)
)
:AddChild(UIElements.New("Button", "resetZoom")
:SetMargin(8, 0, 0, 0)
:SetSize(100, 20)
:SetFont("TABLE_TABLE1")
:SetTextColor("TEXT")
:SetContext(TIME_RANGE_LOOKUP["all"])
:SetText(L["Reset Zoom"])
:SetScript("OnClick", private.TimeBtnOnClick)
)
)
)
:AddChild(UIElements.New("Graph", "goldGraph")
:SetMargin(0, 0, 8, 8)
:SetAxisStepFunctions(private.GraphXStepFunc, private.GraphYStepFunc)
:SetXRange(TSM.Accounting.GoldTracker.GetGraphTimeRange(private.settings.dashboardUnselectedCharacters))
:SetYValueFunction(private.GetGraphYValue)
:SetFormatFunctions(private.GraphFormatX, private.GraphFormatY)
:SetScript("OnZoomChanged", private.GraphOnZoomChanged)
:SetScript("OnHoverUpdate", private.GraphOnHoverUpdate)
)
:AddChild(UIElements.New("Frame", "summary")
:SetLayout("HORIZONTAL")
:SetHeight(48)
:SetBackgroundColor("PRIMARY_BG_ALT", true)
:AddChild(UIElements.New("Frame", "range")
:SetLayout("VERTICAL")
:SetPadding(8, 8, 2, 2)
:AddChild(UIElements.New("Frame", "high")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 0, 4)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetText(L["HIGH"])
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "value")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
)
)
:AddChild(UIElements.New("Frame", "low")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetText(L["LOW"])
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "value")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
)
)
)
:AddChild(UIElements.New("Texture", "line1")
:SetWidth(1)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Frame", "daily")
:SetLayout("VERTICAL")
:SetPadding(8, 8, 2, 2)
:AddChild(UIElements.New("Frame", "sales")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 0, 4)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetText(L["DAILY SALES"])
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "value")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
)
)
:AddChild(UIElements.New("Frame", "purchases")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetText(L["DAILY PURCHASES"])
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "value")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
)
)
)
:AddChild(UIElements.New("Texture", "line2")
:SetWidth(1)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Frame", "top")
:SetLayout("VERTICAL")
:SetPadding(8, 8, 2, 2)
:AddChild(UIElements.New("Frame", "sale")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 0, 4)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetText(L["TOP SALE"])
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "value")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
)
)
:AddChild(UIElements.New("Frame", "expense")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetText(L["TOP PURCHASE"])
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "value")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
)
)
)
)
:AddChild(UIElements.New("Frame", "details")
:SetLayout("VERTICAL")
:SetMargin(0, 0, 8, 0)
:SetPadding(8)
:SetBackgroundColor("PRIMARY_BG_ALT", true)
:AddChild(UIElements.New("Text", "salesLabel")
:SetHeight(20)
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetText(L["SALES"])
)
:AddChild(UIElements.New("Frame", "salesTotal")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:AddChild(UIElements.New("Text", "text")
:SetWidth("AUTO")
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["Total Gold Earned"]..":")
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "amount")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
)
)
:AddChild(UIElements.New("Frame", "salesAvg")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 4, 0)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["Average Earned per Day"]..":")
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "amount")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
)
)
:AddChild(UIElements.New("Frame", "salesTop")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 4, 0)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["Top Item"]..":")
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Button", "item")
:SetWidth("AUTO")
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
)
)
:AddChild(UIElements.New("Texture", "line1")
:SetHeight(1)
:SetMargin(-8, -8, 4, 4)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Text", "expensesLabel")
:SetHeight(20)
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetText(L["EXPENSES"])
)
:AddChild(UIElements.New("Frame", "expensesTotal")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:AddChild(UIElements.New("Text", "text")
:SetWidth("AUTO")
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["Total Gold Spent"]..":")
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "amount")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
)
)
:AddChild(UIElements.New("Frame", "expensesAvg")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 4, 0)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["Average Spent per Day"]..":")
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "amount")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
)
)
:AddChild(UIElements.New("Frame", "expensesTop")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 4, 0)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["Top Item"]..":")
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Button", "item")
:SetWidth("AUTO")
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
)
)
:AddChild(UIElements.New("Texture", "line2")
:SetHeight(1)
:SetMargin(-8, -8, 4, 4)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Text", "profitLabel")
:SetHeight(20)
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetText(L["PROFIT"])
)
:AddChild(UIElements.New("Frame", "profitTotal")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:AddChild(UIElements.New("Text", "text")
:SetWidth("AUTO")
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["Total Profit"]..":")
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "amount")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
)
)
:AddChild(UIElements.New("Frame", "profitAvg")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 4, 0)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["Average Profit per Day"]..":")
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "amount")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
)
)
:AddChild(UIElements.New("Frame", "profitTop")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 4, 0)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["Top Item"]..":")
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Button", "item")
:SetWidth("AUTO")
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
)
)
)
)
frame:GetElement("content.goldHeader.timeBtns.resetZoom"):Hide()
frame:GetElement("content.goldHeader.hoverTime"):Hide()
local newsContent = frame:GetElement("news.content")
local newsEntries = TSM.GetAppNews()
if newsEntries then
for i, info in ipairs(newsEntries) do
newsContent:AddChild(UIElements.New("Frame", "news"..i)
:SetLayout("VERTICAL")
:SetPadding(0, 0, i == 1 and 6 or 12, 0)
:AddChild(UIElements.New("Text", "date")
:SetHeight(20)
:SetFont("BODY_BODY3")
:SetText(date("%b %d, %Y", info.timestamp))
)
:AddChild(UIElements.New("Text", "title")
:SetHeight(20)
:SetFont("BODY_BODY2_BOLD")
:SetText(info.title)
)
:AddChild(UIElements.New("Text", "content")
:SetHeight(80)
:SetPadding(0, 0, 4, 0)
:SetFont("BODY_BODY3")
:SetText(info.content)
)
:AddChild(UIElements.New("Text", "readMore")
:SetHeight(20)
:SetPadding(0, 0, 4, 0)
:SetFont("BODY_BODY3")
:SetTextColor("INDICATOR")
:SetText(L["Read More"])
)
)
:AddChildNoLayout(UIElements.New("Button", "btn")
:AddAnchor("TOPLEFT", "news"..i)
:AddAnchor("BOTTOMRIGHT", "news"..i)
:SetContext(info)
:SetScript("OnClick", private.ButtonOnClick)
)
end
end
return frame
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.ButtonOnClick(button)
local info = button:GetContext()
Analytics.Action("NEWS_READ_MORE", info.title)
button:GetBaseElement():ShowDialogFrame(UIElements.New("Frame", "frame")
:SetLayout("VERTICAL")
:SetSize(600, 450)
:AddAnchor("CENTER")
:SetBackgroundColor("FRAME_BG")
:SetBorderColor("ACTIVE_BG")
:AddChild(UIElements.New("Text", "title")
:SetHeight(44)
:SetMargin(16, 16, 16, 8)
:SetFont("BODY_BODY1_BOLD")
:SetJustifyH("CENTER")
:SetText(info.title)
)
:AddChild(UIElements.New("Input", "linkInput")
:SetHeight(26)
:SetMargin(16, 16, 0, 16)
:SetBackgroundColor("PRIMARY_BG_ALT")
:SetValidateFunc(private.LinkValidateFunc)
:SetContext(info.link)
:SetValue(info.link)
)
:AddChild(UIElements.New("Text", "content")
:SetMargin(16, 16, 0, 16)
:SetFont("BODY_BODY3")
:SetJustifyV("TOP")
:SetText(info.content)
)
:AddChild(UIElements.New("Frame", "buttons")
:SetLayout("HORIZONTAL")
:SetHeight(26)
:SetMargin(16, 16, 0, 16)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("ActionButton", "confirmBtn")
:SetWidth(126)
:SetText(CLOSE)
:SetScript("OnClick", private.DialogCloseBtnOnClick)
)
)
)
end
function private.LinkValidateFunc(input, value)
return value == input:GetContext()
end
function private.DialogCloseBtnOnClick(button)
button:GetBaseElement():HideDialog()
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GraphFormatX(timestamp, suggestedStep)
if suggestedStep > SECONDS_PER_DAY * 14 then
return date("%b '%y", timestamp)
elseif suggestedStep > SECONDS_PER_DAY * 2 then
return date("%b %d", timestamp)
elseif suggestedStep > SECONDS_PER_DAY / 6 then
return date("%a", timestamp)
else
if GetCVar("timeMgrUseMilitaryTime") == "1" then
return date("%H:%M", timestamp)
else
return strtrim(date("%I %p", timestamp), "0")
end
end
end
function private.GraphFormatY(value, suggestedStep, isTooltip)
if isTooltip then
return Money.ToString(value, nil, "OPT_TRIM")
end
if TSM.IsWowClassic() and value < COPPER_PER_GOLD * 1000 then
-- "###g"
return floor(value / COPPER_PER_GOLD)..Money.GetGoldText()
elseif TSM.IsWowClassic() and value < COPPER_PER_GOLD * 1000 * 10 then
-- "#.##Kg"
return format("%.2f", value / (COPPER_PER_GOLD * 1000)).."k"..Money.GetGoldText()
elseif value < COPPER_PER_GOLD * 1000 * 1000 then
-- "###Kg"
return floor(value / (COPPER_PER_GOLD * 1000)).."k"..Money.GetGoldText()
elseif value < COPPER_PER_GOLD * 1000 * 1000 * 10 then
-- "#.##Mg"
return format("%.2f", value / (COPPER_PER_GOLD * 1000 * 1000)).."M"..Money.GetGoldText()
else
-- "###Mg"
return floor(value / (COPPER_PER_GOLD * 1000 * 1000)).."M"..Money.GetGoldText()
end
end
function private.GetGraphYValue(xValue)
return TSM.Accounting.GoldTracker.GetGoldAtTime(xValue, private.settings.dashboardUnselectedCharacters)
end
function private.ContentOnUpdate(contentFrame)
contentFrame:SetScript("OnUpdate", nil)
private.UpdateTimeButtons(contentFrame:GetElement("goldHeader.timeBtns"))
private.UpdateGraph(contentFrame)
end
function private.DropdownOnSelectionChanged(dropdown)
for _, key in ipairs(private.characterGuilds) do
private.settings.dashboardUnselectedCharacters[key] = not dropdown:ItemIsSelectedByKey(key) or nil
end
private.UpdateGraph(dropdown:GetParentElement():GetParentElement())
end
function private.TimeBtnOnClick(button)
local timeRange = button:GetContext()
assert(timeRange)
private.selectedTimeRange = timeRange
private.settings.dashboardTimeRange = timeRange
private.UpdateTimeButtons(button:GetParentElement())
private.UpdateGraph(button:GetParentElement():GetParentElement():GetParentElement())
end
function private.UpdateGraph(contentFrame)
-- update the graph
local minTime, maxTime = TSM.Accounting.GoldTracker.GetGraphTimeRange(private.settings.dashboardUnselectedCharacters)
local goldGraph = contentFrame:GetElement("goldGraph")
local zoomStart, zoomEnd = goldGraph:GetZoom()
if private.selectedTimeRange == TIME_RANGE_LOOKUP["all"] then
zoomStart = minTime
zoomEnd = maxTime
elseif private.selectedTimeRange then
zoomStart = max(time() - private.selectedTimeRange, minTime)
zoomEnd = time()
end
goldGraph:SetXRange(minTime, maxTime)
:SetZoom(zoomStart, zoomEnd)
:Draw()
private.PopulateDetails(contentFrame)
end
function private.GraphOnZoomChanged(graph)
private.selectedTimeRange = nil
private.settings.dashboardTimeRange = -1
private.UpdateTimeButtons(graph:GetElement("__parent.goldHeader.timeBtns"))
private.PopulateDetails(graph:GetElement("__parent"))
end
function private.GraphOnHoverUpdate(graph, hoverTime)
local goldHeader = graph:GetElement("__parent.goldHeader")
if hoverTime then
local timeStr = nil
if GetCVar("timeMgrUseMilitaryTime") == "1" then
timeStr = date("%H:%M %b %d, %Y", hoverTime)
else
timeStr = gsub(date("%I:%M %p %b %d, %Y", hoverTime), "^0", "")
end
goldHeader:GetElement("timeBtns"):Hide()
goldHeader:GetElement("hoverTime")
:SetText(timeStr)
:Show()
else
goldHeader:GetElement("timeBtns"):Show()
goldHeader:GetElement("hoverTime"):Hide()
private.UpdateTimeButtons(goldHeader:GetElement("timeBtns"))
end
goldHeader:Draw()
end
function private.UpdateTimeButtons(frame)
frame:ShowAllChildren()
local resetButton = frame:GetElement("resetZoom")
if private.selectedTimeRange then
for _, button in frame:LayoutChildrenIterator() do
if button ~= resetButton then
button:SetTextColor(private.selectedTimeRange == button:GetContext() and "TEXT" or "ACTIVE_BG_ALT")
end
end
resetButton:Hide()
else
for _, button in frame:LayoutChildrenIterator() do
button:Hide()
end
resetButton:Show()
end
frame:GetParentElement():Draw()
end
function private.GraphXStepFunc(prevValue, suggestedStep)
local year, day, month, hour, min, sec = strsplit(",", date("%Y,%d,%m,%H,%M,%S", prevValue))
private.tempTimeTable.year = tonumber(year)
private.tempTimeTable.day = tonumber(day)
private.tempTimeTable.month = tonumber(month)
private.tempTimeTable.hour = tonumber(hour)
private.tempTimeTable.min = tonumber(min)
private.tempTimeTable.sec = tonumber(sec)
if suggestedStep > SECONDS_PER_DAY * 14 then
private.tempTimeTable.month = private.tempTimeTable.month + 1
private.tempTimeTable.day = 1
private.tempTimeTable.hour = 0
private.tempTimeTable.min = 0
private.tempTimeTable.sec = 0
elseif suggestedStep > SECONDS_PER_DAY / 6 then
private.tempTimeTable.day = private.tempTimeTable.day + 1
if private.tempTimeTable.hour == 23 then
-- add an extra hour to avoid DST issues
private.tempTimeTable.hour = 1
else
private.tempTimeTable.hour = 0
end
private.tempTimeTable.min = 0
private.tempTimeTable.sec = 0
else
private.tempTimeTable.hour = private.tempTimeTable.hour + 1
private.tempTimeTable.min = 0
private.tempTimeTable.sec = 0
end
local newValue = time(private.tempTimeTable)
assert(newValue > prevValue)
return newValue
end
function private.GraphYStepFunc(mode, ...)
if mode == "RANGE" then
local yMin, yMax, maxNumSteps = ...
-- find the smallest 10^X step size which still looks good
local minStep = max((yMax - yMin) / maxNumSteps / 10, yMax / 200)
local stepSize = MIN_GRAPH_STEP_SIZE
while stepSize < minStep do
stepSize = stepSize * 10
end
yMin = Math.Floor(yMin, stepSize)
yMax = Math.Ceil(yMax + stepSize / 3, stepSize)
if yMin == yMax then
yMax = yMax + stepSize
end
return yMin, yMax
elseif mode == "NEXT" then
local prevValue, yMax = ...
local stepSize = MIN_GRAPH_STEP_SIZE
while stepSize < yMax / 1000 do
stepSize = stepSize * 10
end
return Math.Floor(prevValue, stepSize) + stepSize
else
error("Invalid mode")
end
end
function private.PopulateDetails(contentFrame)
local goldGraph = contentFrame:GetElement("goldGraph")
local unselectedCharacters = next(private.settings.dashboardUnselectedCharacters) and private.settings.dashboardUnselectedCharacters or nil
local timeFilterStart, timeFilterEnd, numDays = nil, nil, nil
if private.selectedTimeRange and private.selectedTimeRange ~= -1 then
timeFilterStart = time() - private.selectedTimeRange
timeFilterEnd = time()
numDays = ceil(private.selectedTimeRange / SECONDS_PER_DAY)
elseif not private.selectedTimeRange then
timeFilterStart, timeFilterEnd = goldGraph:GetZoom()
numDays = ceil((timeFilterEnd - timeFilterStart) / SECONDS_PER_DAY)
else
local timeStart, timeEnd = goldGraph:GetXRange()
numDays = ceil((timeEnd - timeStart) / SECONDS_PER_DAY)
end
numDays = max(numDays, 1)
local saleTotal, salePerDay, saleTopItem, saleTopValue, saleTotalQuantity = 0, nil, nil, 0, 0
local buyTotal, buyPerDay, buyTopItem, buyTopValue, buyTotalQuantity = 0, nil, nil, 0, 0
local profitTopItem = nil
local query = TSM.Accounting.GetSummaryQuery(timeFilterStart, timeFilterEnd, unselectedCharacters)
local saleNumDays, buyNumDays = 1, 1
local saleItemTotals = TempTable.Acquire()
local buyItemTotals = TempTable.Acquire()
local saleItemNum = TempTable.Acquire()
local buyItemNum = TempTable.Acquire()
for _, recordType, itemString, price, quantity, timestamp in query:Iterator() do
if recordType == "sale" then
local daysAgo = floor((time() - timestamp) / (24 * 60 * 60))
saleNumDays = max(saleNumDays, daysAgo)
saleItemTotals[itemString] = (saleItemTotals[itemString] or 0) + price * quantity
saleTopValue = max(saleTopValue, price)
saleTotalQuantity = saleTotalQuantity + quantity
saleItemNum[itemString] = (saleItemNum[itemString] or 0) + quantity
elseif recordType == "buy" then
local daysAgo = floor((time() - timestamp) / (24 * 60 * 60))
buyNumDays = max(buyNumDays, daysAgo)
buyItemTotals[itemString] = (buyItemTotals[itemString] or 0) + price * quantity
buyTopValue = max(buyTopValue, price)
buyTotalQuantity = buyTotalQuantity + quantity
buyItemNum[itemString] = (buyItemNum[itemString] or 0) + quantity
else
error("Invalid recordType: "..tostring(recordType))
end
end
query:Release()
local topSaleItemTotal = 0
for itemString, itemTotal in pairs(saleItemTotals) do
saleTotal = saleTotal + itemTotal
if itemTotal > topSaleItemTotal then
saleTopItem = itemString
topSaleItemTotal = itemTotal
end
end
salePerDay = Math.Round(saleTotal / saleNumDays)
local topBuyItemTotal = 0
for itemString, itemTotal in pairs(buyItemTotals) do
buyTotal = buyTotal + itemTotal
if itemTotal > topBuyItemTotal then
buyTopItem = itemString
topBuyItemTotal = itemTotal
end
end
buyPerDay = Math.Round(buyTotal / buyNumDays)
local topItemProfit = 0
for itemString in pairs(saleItemNum) do
if buyItemNum[itemString] then
local profit = (saleItemTotals[itemString] / saleItemNum[itemString] - buyItemTotals[itemString] / buyItemNum[itemString]) * min(saleItemNum[itemString], buyItemNum[itemString])
if profit > topItemProfit then
profitTopItem = itemString
topItemProfit = profit
end
end
end
TempTable.Release(saleItemTotals)
TempTable.Release(buyItemTotals)
TempTable.Release(saleItemNum)
TempTable.Release(buyItemNum)
local profitTotal = saleTotal - buyTotal
local profitPerDay = salePerDay - buyPerDay
local rangeLow, rangeHigh = goldGraph:GetYRange()
contentFrame:GetElement("summary.range.low.value")
:SetText(Money.ToString(rangeLow, nil, "OPT_TRIM") or "-")
contentFrame:GetElement("summary.range.high.value")
:SetText(Money.ToString(rangeHigh, nil, "OPT_TRIM") or "-")
contentFrame:GetElement("summary.daily.sales.value")
:SetText(saleTotalQuantity and Math.Round(saleTotalQuantity / numDays) or "-")
contentFrame:GetElement("summary.daily.purchases.value")
:SetText(buyTotalQuantity and Math.Round(buyTotalQuantity / numDays) or "-")
contentFrame:GetElement("summary.top.sale.value")
:SetText(Money.ToString(Math.Round(saleTopValue, TSM.IsWowClassic() and 1 or COPPER_PER_GOLD), nil, "OPT_TRIM") or "-")
contentFrame:GetElement("summary.top.expense.value")
:SetText(Money.ToString(Math.Round(buyTopValue, TSM.IsWowClassic() and 1 or COPPER_PER_GOLD), nil, "OPT_TRIM") or "-")
contentFrame:GetElement("details.salesTotal.amount")
:SetText(Money.ToString(saleTotal))
contentFrame:GetElement("details.salesAvg.amount")
:SetText(Money.ToString(salePerDay))
contentFrame:GetElement("details.salesTop.item")
:SetText(TSM.UI.GetColoredItemName(saleTopItem) or "-")
:SetTooltip(saleTopItem)
contentFrame:GetElement("details.expensesTotal.amount")
:SetText(Money.ToString(buyTotal))
contentFrame:GetElement("details.expensesAvg.amount")
:SetText(Money.ToString(buyPerDay))
contentFrame:GetElement("details.expensesTop.item")
:SetText(TSM.UI.GetColoredItemName(buyTopItem) or "-")
:SetTooltip(buyTopItem)
contentFrame:GetElement("details.profitTotal.amount")
:SetText(Money.ToString(profitTotal, profitTotal < 0 and Theme.GetFeedbackColor("RED"):GetTextColorPrefix() or nil))
contentFrame:GetElement("details.profitAvg.amount")
:SetText(Money.ToString(profitPerDay, profitPerDay < 0 and Theme.GetFeedbackColor("RED"):GetTextColorPrefix() or nil))
contentFrame:GetElement("details.profitTop.item")
:SetText(TSM.UI.GetColoredItemName(profitTopItem) or "-")
:SetTooltip(profitTopItem)
contentFrame:Draw()
end

1870
Core/UI/MainUI/Groups.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,266 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Auctions = TSM.MainUI.Ledger.Common:NewPackage("Auctions")
local L = TSM.Include("Locale").GetTable()
local Table = TSM.Include("Util.Table")
local String = TSM.Include("Util.String")
local Theme = TSM.Include("Util.Theme")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
local SECONDS_PER_DAY = 24 * 60 * 60
local private = {
settings = nil,
query = nil,
characters = {},
characterFilter = {},
searchFilter = "",
groupFilter = {},
rarityList = {},
rarityFilter = {},
timeFrameFilter = 30 * SECONDS_PER_DAY,
type = nil
}
do
for i = 1, 4 do
tinsert(private.rarityList, _G[format("ITEM_QUALITY%d_DESC", i)])
private.rarityFilter[i] = true
end
end
local TIME_LIST = { L["All Time"], L["Last 3 Days"], L["Last 7 Days"], L["Last 14 Days"], L["Last 30 Days"], L["Last 60 Days"] }
local TIME_KEYS = { 0, 3 * SECONDS_PER_DAY, 7 * SECONDS_PER_DAY, 14 * SECONDS_PER_DAY, 30 * SECONDS_PER_DAY, 60 * SECONDS_PER_DAY }
-- ============================================================================
-- Module Functions
-- ============================================================================
function Auctions.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "mainUIContext", "ledgerAuctionsScrollingTable")
TSM.MainUI.Ledger.FailedAuctions.RegisterPage(L["Expired"], private.DrawExpiredPage)
TSM.MainUI.Ledger.FailedAuctions.RegisterPage(L["Cancelled"], private.DrawCancelledPage)
end
-- ============================================================================
-- Auctions UIs
-- ============================================================================
function private.DrawExpiredPage()
TSM.UI.AnalyticsRecordPathChange("main", "ledger", "failed_auctions", "expired")
private.type = "expire"
return private.DrawAuctionsPage()
end
function private.DrawCancelledPage()
TSM.UI.AnalyticsRecordPathChange("main", "ledger", "failed_auctions", "cancelled")
private.type = "cancel"
return private.DrawAuctionsPage()
end
function private.DrawAuctionsPage()
private.query = private.query or TSM.Accounting.Auctions.CreateQuery()
private.query:Reset()
:Equal("type", "cancel")
:Distinct("player")
:Select("player")
wipe(private.characters)
for _, character in private.query:Iterator() do
tinsert(private.characters, character)
private.characterFilter[character] = true
end
private.query:Reset()
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
:LeftJoin(TSM.Groups.GetItemDBForJoin(), "itemString")
:OrderBy("time", false)
private.UpdateQuery()
return UIElements.New("Frame", "content")
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Frame", "row1")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(8)
:AddChild(UIElements.New("Input", "filter")
:SetMargin(0, 8, 0, 0)
:SetIconTexture("iconPack.18x18/Search")
:SetClearButtonEnabled(true)
:AllowItemInsert()
:SetHintText(L["Filter by keyword"])
:SetValue(private.searchFilter)
:SetScript("OnValueChanged", private.SearchFilterChanged)
)
:AddChild(UIElements.New("GroupSelector", "group")
:SetWidth(240)
:SetHintText(L["Filter by groups"])
:SetScript("OnSelectionChanged", private.GroupFilterChanged)
)
)
:AddChild(UIElements.New("Frame", "row2")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(8, 8, 0, 8)
:AddChild(UIElements.New("MultiselectionDropdown", "rarity")
:SetMargin(0, 8, 0, 0)
:SetItems(private.rarityList)
:SetSettingInfo(private, "rarityFilter")
:SetSelectionText(L["No Rarities"], L["%d Rarities"], L["All Rarites"])
:SetScript("OnSelectionChanged", private.DropdownCommonOnSelectionChanged)
)
:AddChild(UIElements.New("MultiselectionDropdown", "character")
:SetMargin(0, 8, 0, 0)
:SetItems(private.characters, private.characters)
:SetSettingInfo(private, "characterFilter")
:SetSelectionText(L["No Characters"], L["%d Characters"], L["All Characters"])
:SetScript("OnSelectionChanged", private.DropdownCommonOnSelectionChanged)
)
:AddChild(UIElements.New("SelectionDropdown", "time")
:SetItems(TIME_LIST, TIME_KEYS)
:SetSelectedItemByKey(private.timeFrameFilter)
:SetSettingInfo(private, "timeFrameFilter")
:SetScript("OnSelectionChanged", private.DropdownCommonOnSelectionChanged)
)
)
:AddChild(UIElements.New("QueryScrollingTable", "scrollingTable")
:SetSettingsContext(private.settings, "ledgerAuctionsScrollingTable")
:GetScrollingTableInfo()
:NewColumn("item")
:SetTitle(L["Item"])
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("itemString", TSM.UI.GetColoredItemName)
:SetTooltipInfo("itemString")
:SetSortInfo("name")
:DisableHiding()
:Commit()
:NewColumn("player")
:SetTitle(PLAYER)
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("player")
:SetSortInfo("player")
:Commit()
:NewColumn("stackSize")
:SetTitle(L["Stack"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("stackSize")
:SetSortInfo("stackSize")
:Commit()
:NewColumn("quantity")
:SetTitle(L["Auctions"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo(nil, private.FormatAuctions)
:SetSortInfo("quantity")
:Commit()
:NewColumn("time")
:SetTitle(L["Time Frame"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("time", private.TableGetTimeframeText)
:SetSortInfo("time")
:Commit()
:Commit()
:SetQuery(private.query)
:SetScript("OnRowClick", private.TableSelectionChanged)
)
:AddChild(UIElements.New("Texture", "line")
:SetHeight(2)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Frame", "footer")
:SetLayout("HORIZONTAL")
:SetHeight(40)
:SetPadding(8)
:SetBackgroundColor("PRIMARY_BG")
:AddChild(UIElements.New("Text", "num")
:SetWidth("AUTO")
:SetFont("BODY_BODY2_MEDIUM")
:SetText(format(private.type == "expire" and L["%s Items Expired"] or L["%s Items Cancelled"], Theme.GetColor("INDICATOR"):ColorText(FormatLargeNumber(private.query:Sum("quantity") or 0))))
)
:AddChild(UIElements.New("Spacer", "spacer"))
)
end
-- ============================================================================
-- Scrolling Table Helper Functions
-- ============================================================================
function private.TableGetTimeframeText(record)
return SecondsToTime(time() - record)
end
function private.FormatAuctions(row)
return row:GetField("quantity") / row:GetField("stackSize")
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.DropdownCommonOnSelectionChanged(dropdown)
private.UpdateQuery()
dropdown:GetElement("__parent.__parent.scrollingTable")
:UpdateData(true)
local footer = dropdown:GetElement("__parent.__parent.footer")
footer:GetElement("num"):SetText(format(private.type == "expire" and L["%s Items Expired"] or L["%s Items Cancelled"], Theme.GetColor("INDICATOR"):ColorText(FormatLargeNumber(private.query:Sum("quantity") or 0))))
footer:Draw()
end
function private.SearchFilterChanged(input)
private.searchFilter = input:GetValue()
private.DropdownCommonOnSelectionChanged(input)
end
function private.GroupFilterChanged(groupSelector)
wipe(private.groupFilter)
for groupPath in groupSelector:SelectedGroupIterator() do
private.groupFilter[groupPath] = true
end
private.DropdownCommonOnSelectionChanged(groupSelector)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.UpdateQuery()
private.query:ResetFilters()
:Equal("type", private.type)
if private.searchFilter ~= "" then
private.query:Matches("name", String.Escape(private.searchFilter))
end
if Table.Count(private.rarityFilter) ~= #private.rarityList then
private.query:InTable("quality", private.rarityFilter)
end
if Table.Count(private.characterFilter) ~= #private.characters then
private.query:InTable("player", private.characterFilter)
end
if private.timeFrameFilter ~= 0 then
private.query:GreaterThanOrEqual("time", time() - private.timeFrameFilter)
end
if next(private.groupFilter) then
private.query:InTable("groupPath", private.groupFilter)
end
end
function private.TableSelectionChanged(scrollingTable, row)
TSM.MainUI.Ledger.ShowItemDetail(scrollingTable:GetParentElement():GetParentElement(), row:GetField("itemString"), "sale")
end

View File

@@ -0,0 +1,8 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
TSM.MainUI.Ledger:NewPackage("Common")

View File

@@ -0,0 +1,208 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Other = TSM.MainUI.Ledger.Common:NewPackage("Other")
local L = TSM.Include("Locale").GetTable()
local Table = TSM.Include("Util.Table")
local Money = TSM.Include("Util.Money")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
local SECONDS_PER_DAY = 24 * 60 * 60
local private = {
settings = nil,
query = nil,
characters = {},
characterFilter = {},
typeFilter = {},
recordType = nil,
timeFrameFilter = 30 * SECONDS_PER_DAY,
}
local TIME_LIST = { L["All Time"], L["Last 3 Days"], L["Last 7 Days"], L["Last 14 Days"], L["Last 30 Days"], L["Last 60 Days"] }
local TIME_KEYS = { 0, 3 * SECONDS_PER_DAY, 7 * SECONDS_PER_DAY, 14 * SECONDS_PER_DAY, 30 * SECONDS_PER_DAY, 60 * SECONDS_PER_DAY }
local TYPE_LIST = {
expense = { L["Money Transfer"], L["Postage"], L["Repair Bill"] },
income = { L["Money Transfer"], L["Garrison"] },
}
local TYPE_KEYS = {
expense = { "Money Transfer", "Postage", "Repair Bill" },
income = { "Money Transfer", "Garrison" },
}
local TYPE_STR_LOOKUP = {}
do
-- populate lookup table
assert(#TYPE_LIST.expense == #TYPE_KEYS.expense)
for i = 1, #TYPE_LIST.expense do
TYPE_STR_LOOKUP[TYPE_KEYS.expense[i]] = TYPE_LIST.expense[i]
end
assert(#TYPE_LIST.income == #TYPE_KEYS.income)
for i = 1, #TYPE_LIST.income do
TYPE_STR_LOOKUP[TYPE_KEYS.income[i]] = TYPE_LIST.income[i]
end
end
-- ============================================================================
-- Module Functions
-- ============================================================================
function Other.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "mainUIContext", "ledgerOtherScrollingTable")
TSM.MainUI.Ledger.Expenses.RegisterPage(OTHER, private.DrawOtherExpensesPage)
TSM.MainUI.Ledger.Revenue.RegisterPage(OTHER, private.DrawOtherRevenuePage)
end
-- ============================================================================
-- Other UIs
-- ============================================================================
function private.DrawOtherExpensesPage()
TSM.UI.AnalyticsRecordPathChange("main", "ledger", "expenses", "other")
return private.DrawOtherPage("expense")
end
function private.DrawOtherRevenuePage()
TSM.UI.AnalyticsRecordPathChange("main", "ledger", "revenue", "other")
return private.DrawOtherPage("income")
end
function private.DrawOtherPage(recordType)
wipe(private.characters)
for _, character in TSM.Accounting.Money.CharacterIterator(recordType) do
tinsert(private.characters, character)
private.characterFilter[character] = true
end
wipe(private.typeFilter)
for _, key in ipairs(TYPE_KEYS[recordType]) do
private.typeFilter[key] = true
end
if not private.query then
private.query = TSM.Accounting.Money.CreateQuery()
:OrderBy("time", false)
end
private.recordType = recordType
private.UpdateQuery()
return UIElements.New("Frame", "content")
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Frame", "row2")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(8)
:AddChild(UIElements.New("MultiselectionDropdown", "type")
:SetMargin(0, 8, 0, 0)
:SetItems(TYPE_LIST[recordType], TYPE_KEYS[recordType])
:SetSettingInfo(private, "typeFilter")
:SetSelectionText(L["No Types"], L["%d Types"], L["All Types"])
:SetScript("OnSelectionChanged", private.DropdownChangedCommon)
)
:AddChild(UIElements.New("MultiselectionDropdown", "character")
:SetMargin(0, 8, 0, 0)
:SetItems(private.characters, private.characters)
:SetSettingInfo(private, "characterFilter")
:SetSelectionText(L["No Characters"], L["%d Characters"], L["All Characters"])
:SetScript("OnSelectionChanged", private.DropdownChangedCommon)
)
:AddChild(UIElements.New("SelectionDropdown", "time")
:SetItems(TIME_LIST, TIME_KEYS)
:SetSelectedItemByKey(private.timeFrameFilter)
:SetSettingInfo(private, "timeFrameFilter")
:SetScript("OnSelectionChanged", private.DropdownChangedCommon)
)
)
:AddChild(UIElements.New("QueryScrollingTable", "table")
:SetSettingsContext(private.settings, "ledgerOtherScrollingTable")
:GetScrollingTableInfo()
:NewColumn("type")
:SetTitle(L["Type"])
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("type", private.TableGetTypeText)
:SetSortInfo("type")
:Commit()
:NewColumn("character")
:SetTitle(L["Character"])
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("player")
:SetSortInfo("player")
:Commit()
:NewColumn("otherCharacter")
:SetTitle(L["Other Character"])
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("otherPlayer")
:SetSortInfo("otherPlayer")
:Commit()
:NewColumn("amount")
:SetTitle(L["Amount"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("amount", Money.ToString)
:SetSortInfo("amount")
:Commit()
:NewColumn("time")
:SetTitle(L["Time Frame"])
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("time", private.TableGetTimeText)
:SetSortInfo("time")
:Commit()
:Commit()
:SetQuery(private.query)
:SetSelectionDisabled(true)
)
end
-- ============================================================================
-- Scrolling Table Helper Functions
-- ============================================================================
function private.TableGetTypeText(typeValue)
return TYPE_STR_LOOKUP[typeValue]
end
function private.TableGetTimeText(timevalue)
return SecondsToTime(time() - timevalue)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.DropdownChangedCommon(dropdown)
private.UpdateQuery()
dropdown:GetElement("__parent.__parent.table"):UpdateData(true)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.UpdateQuery()
private.query:ResetFilters()
:Equal("recordType", private.recordType)
if Table.Count(private.typeFilter) ~= #TYPE_KEYS[private.recordType] then
private.query:InTable("type", private.typeFilter)
end
if Table.Count(private.characterFilter) ~= #private.characters then
private.query:InTable("player", private.characterFilter)
end
if private.timeFrameFilter ~= 0 then
private.query:GreaterThan("time", time() - private.timeFrameFilter)
end
end

View File

@@ -0,0 +1,329 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Transactions = TSM.MainUI.Ledger.Common:NewPackage("Transactions")
local L = TSM.Include("Locale").GetTable()
local Money = TSM.Include("Util.Money")
local String = TSM.Include("Util.String")
local Table = TSM.Include("Util.Table")
local Theme = TSM.Include("Util.Theme")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
local SECONDS_PER_DAY = 24 * 60 * 60
local private = {
settings = nil,
query = nil,
characters = {},
characterFilter = {},
typeFilter = {},
searchFilter = "",
groupFilter = {},
rarityList = {},
rarityFilter = {},
timeFrameFilter = 30 * SECONDS_PER_DAY,
type = nil
}
local TYPE_LIST = { L["Auction"], COD, TRADE, L["Vendor"] }
local TYPE_KEYS = { "Auction", "COD", "Trade", "Vendor" }
do
for _, key in ipairs(TYPE_KEYS) do
private.typeFilter[key] = true
end
for i = 1, 4 do
tinsert(private.rarityList, _G[format("ITEM_QUALITY%d_DESC", i)])
private.rarityFilter[i] = true
end
end
local TIME_LIST = { L["All Time"], L["Last 3 Days"], L["Last 7 Days"], L["Last 14 Days"], L["Last 30 Days"], L["Last 60 Days"] }
local TIME_KEYS = { 0, 3 * SECONDS_PER_DAY, 7 * SECONDS_PER_DAY, 14 * SECONDS_PER_DAY, 30 * SECONDS_PER_DAY, 60 * SECONDS_PER_DAY }
-- ============================================================================
-- Module Functions
-- ============================================================================
function Transactions.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "mainUIContext", "ledgerTransactionsScrollingTable")
TSM.MainUI.Ledger.Expenses.RegisterPage(L["Purchases"], private.DrawPurchasesPage)
TSM.MainUI.Ledger.Revenue.RegisterPage(L["Sales"], private.DrawSalesPage)
end
-- ============================================================================
-- Transactions UIs
-- ============================================================================
function private.DrawPurchasesPage()
TSM.UI.AnalyticsRecordPathChange("main", "ledger", "expenses", "purchases")
private.type = "buy"
return private.DrawTransactionPage()
end
function private.DrawSalesPage()
TSM.UI.AnalyticsRecordPathChange("main", "ledger", "revenue", "sales")
private.type = "sale"
return private.DrawTransactionPage()
end
function private.DrawTransactionPage()
private.query = private.query or TSM.Accounting.Transactions.CreateQuery()
private.query:Reset()
:Equal("type", private.type)
:Distinct("player")
:Select("player")
wipe(private.characters)
for _, character in private.query:Iterator() do
tinsert(private.characters, character)
private.characterFilter[character] = true
end
private.query:Reset()
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
:LeftJoin(TSM.Groups.GetItemDBForJoin(), "itemString")
:VirtualField("total", "number", private.GetTotal)
:VirtualField("auctions", "number", private.GetAuctions)
:OrderBy("time", false)
private.UpdateQuery()
local numItems = private.query:Sum("quantity") or 0
local total = private.query:Sum("total") or 0
return UIElements.New("Frame", "content")
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Frame", "row1")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(8)
:AddChild(UIElements.New("Input", "filter")
:SetMargin(0, 8, 0, 0)
:SetIconTexture("iconPack.18x18/Search")
:SetClearButtonEnabled(true)
:AllowItemInsert()
:SetHintText(L["Filter by keyword"])
:SetValue(private.searchFilter)
:SetScript("OnValueChanged", private.SearchFilterChanged)
)
:AddChild(UIElements.New("GroupSelector", "group")
:SetWidth(240)
:SetHintText(L["Filter by groups"])
:SetScript("OnSelectionChanged", private.GroupFilterChanged)
)
)
:AddChild(UIElements.New("Frame", "row2")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(8, 8, 0, 8)
:AddChild(UIElements.New("MultiselectionDropdown", "type")
:SetMargin(0, 8, 0, 0)
:SetItems(TYPE_LIST, TYPE_KEYS)
:SetSettingInfo(private, "typeFilter")
:SetSelectionText(L["No Types"], L["%d Types"], L["All Types"])
:SetScript("OnSelectionChanged", private.DropdownCommonOnSelectionChanged)
)
:AddChild(UIElements.New("MultiselectionDropdown", "rarity")
:SetMargin(0, 8, 0, 0)
:SetItems(private.rarityList)
:SetSettingInfo(private, "rarityFilter")
:SetSelectionText(L["No Rarities"], L["%d Rarities"], L["All Rarities"])
:SetScript("OnSelectionChanged", private.DropdownCommonOnSelectionChanged)
)
:AddChild(UIElements.New("MultiselectionDropdown", "character")
:SetMargin(0, 8, 0, 0)
:SetItems(private.characters, private.characters)
:SetSettingInfo(private, "characterFilter")
:SetSelectionText(L["No Characters"], L["%d Characters"], L["All Characters"])
:SetScript("OnSelectionChanged", private.DropdownCommonOnSelectionChanged)
)
:AddChild(UIElements.New("SelectionDropdown", "time")
:SetItems(TIME_LIST, TIME_KEYS)
:SetSelectedItemByKey(private.timeFrameFilter)
:SetSettingInfo(private, "timeFrameFilter")
:SetScript("OnSelectionChanged", private.DropdownCommonOnSelectionChanged)
)
)
:AddChild(UIElements.New("QueryScrollingTable", "scrollingTable")
:SetSettingsContext(private.settings, "ledgerTransactionsScrollingTable")
:GetScrollingTableInfo()
:NewColumn("item")
:SetTitle(L["Item"])
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("itemString", TSM.UI.GetColoredItemName)
:SetTooltipInfo("itemString")
:SetSortInfo("name")
:DisableHiding()
:Commit()
:NewColumn("player")
:SetTitle(PLAYER)
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("otherPlayer")
:SetSortInfo("otherPlayer")
:Commit()
:NewColumn("type")
:SetTitle(L["Type"])
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("source")
:SetSortInfo("source")
:Commit()
:NewColumn("stack")
:SetTitle(L["Stack"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("stackSize")
:SetSortInfo("stackSize")
:Commit()
:NewColumn("auctions")
:SetTitle(L["Auctions"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("auctions")
:SetSortInfo("auctions")
:Commit()
:NewColumn("perItem")
:SetTitle(L["Per Item"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("price", private.TableGetPriceText)
:SetSortInfo("price")
:Commit()
:NewColumn("total")
:SetTitle(L["Total"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("total", private.TableGetPriceText)
:SetSortInfo("total")
:Commit()
:NewColumn("time")
:SetTitle(L["Time Frame"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("time", private.TableGetTimeframeText)
:SetSortInfo("time")
:Commit()
:Commit()
:SetQuery(private.query)
:SetScript("OnRowClick", private.TableSelectionChanged)
)
:AddChild(UIElements.New("Texture", "line")
:SetHeight(2)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Frame", "footer")
:SetLayout("HORIZONTAL")
:SetHeight(40)
:SetPadding(8)
:SetBackgroundColor("PRIMARY_BG")
:AddChild(UIElements.New("Text", "num")
:SetWidth("AUTO")
:SetFont("BODY_BODY2_MEDIUM")
:SetText(format(private.type == "sale" and L["%s Items Sold"] or L["%s Items Bought"], Theme.GetColor("INDICATOR"):ColorText(FormatLargeNumber(numItems))))
)
:AddChild(UIElements.New("Texture", "line")
:SetMargin(4, 8, 0, 0)
:SetWidth(2)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Text", "profit")
:SetWidth("AUTO")
:SetFont("BODY_BODY2_MEDIUM")
:SetText(format(L["%s Total"], Money.ToString(total)))
)
:AddChild(UIElements.New("Spacer", "spacer"))
)
end
-- ============================================================================
-- Scrolling Table Helper Functions
-- ============================================================================
function private.TableGetPriceText(price)
return Money.ToString(price)
end
function private.TableGetTimeframeText(record)
return SecondsToTime(time() - record)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.DropdownCommonOnSelectionChanged(dropdown)
private.UpdateQuery()
local numItems = private.query:Sum("quantity") or 0
local total = private.query:Sum("total") or 0
dropdown:GetElement("__parent.__parent.scrollingTable")
:UpdateData(true)
local footer = dropdown:GetElement("__parent.__parent.footer")
footer:GetElement("num"):SetText(format(private.type == "sale" and L["%s Items Sold"] or L["%s Items Bought"], Theme.GetColor("INDICATOR"):ColorText(FormatLargeNumber(numItems))))
footer:GetElement("profit"):SetText(format(L["%s Total"], Money.ToString(total)))
footer:Draw()
end
function private.SearchFilterChanged(input)
private.searchFilter = input:GetValue()
private.DropdownCommonOnSelectionChanged(input)
end
function private.GroupFilterChanged(groupSelector)
wipe(private.groupFilter)
for groupPath in groupSelector:SelectedGroupIterator() do
private.groupFilter[groupPath] = true
end
private.DropdownCommonOnSelectionChanged(groupSelector)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetTotal(row)
return row:GetField("price") * row:GetField("quantity")
end
function private.GetAuctions(row)
return row:GetField("quantity") / row:GetField("stackSize")
end
function private.UpdateQuery()
private.query:ResetFilters()
:Equal("type", private.type)
if private.searchFilter ~= "" then
private.query:Matches("name", String.Escape(private.searchFilter))
end
if Table.Count(private.typeFilter) ~= #TYPE_KEYS then
private.query:InTable("source", private.typeFilter)
end
if Table.Count(private.rarityFilter) ~= #private.rarityList then
private.query:InTable("quality", private.rarityFilter)
end
if Table.Count(private.characterFilter) ~= #private.characters then
private.query:InTable("player", private.characterFilter)
end
if private.timeFrameFilter ~= 0 then
private.query:GreaterThan("time", time() - private.timeFrameFilter)
end
if next(private.groupFilter) then
private.query:InTable("groupPath", private.groupFilter)
end
end
function private.TableSelectionChanged(scrollingTable, row)
TSM.MainUI.Ledger.ShowItemDetail(scrollingTable:GetParentElement():GetParentElement(), row:GetField("itemString"), private.type)
end

View File

@@ -0,0 +1,524 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Ledger = TSM.MainUI:NewPackage("Ledger")
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
local Table = TSM.Include("Util.Table")
local Money = TSM.Include("Util.Money")
local Theme = TSM.Include("Util.Theme")
local Log = TSM.Include("Util.Log")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
local SECONDS_PER_DAY = 24 * 60 * 60
local private = {
settings = nil,
pages = {},
childPages = {},
callback = {},
contextPath = nil,
contextItemString = nil,
itemDetailType = "sale",
}
local NUM_TOP_PLAYERS = 3
local PAGE_PATH_SEP = "`"
-- ============================================================================
-- Module Functions
-- ============================================================================
function Ledger.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "mainUIContext", "ledgerDetailScrollingTable")
TSM.MainUI.RegisterTopLevelPage(L["Ledger"], private.GetLedgerFrame)
end
function Ledger.RegisterPage(name, callback)
tinsert(private.pages, name)
private.callback[name] = callback
end
function Ledger.RegisterChildPage(parentName, childName, callback)
local path = parentName..PAGE_PATH_SEP..childName
private.childPages[parentName] = private.childPages[parentName] or {}
tinsert(private.childPages[parentName], childName)
private.callback[path] = callback
end
function Ledger.ShowItemDetail(frame, itemString, detailType)
assert(detailType == "sale" or detailType == "buy")
private.contextItemString = itemString
private.itemDetailType = detailType
frame:SetPath("itemDetail", true)
end
-- ============================================================================
-- Ledger UI
-- ============================================================================
function private.GetLedgerFrame()
TSM.UI.AnalyticsRecordPathChange("main", "ledger")
local defaultPage = private.pages[1]
local frame = UIElements.New("Frame", "ledger")
:SetLayout("HORIZONTAL")
:SetBackgroundColor("PRIMARY_BG_ALT")
:AddChild(UIElements.New("Frame", "navigation")
:SetLayout("VERTICAL")
:SetWidth(160)
:SetPadding(12, 12, 1, 9)
)
:AddChild(UIElements.New("Texture", "divider")
:SetWidth(2)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Frame", "contentFrame")
:SetLayout("VERTICAL")
:AddChild(UIElements.New("ViewContainer", "content")
:SetNavCallback(private.ContentNavCallback)
:AddPath("itemDetail")
)
)
:SetScript("OnHide", private.NavButtonOnHide)
local content = frame:GetElement("contentFrame.content")
local navFrame = frame:GetElement("navigation")
for _, pageName in ipairs(private.pages) do
navFrame:AddChild(UIElements.New("Button", pageName)
:SetHeight(20)
:SetMargin(0, 0, 8, 0)
:SetFont("BODY_BODY2_BOLD")
:SetJustifyH("LEFT")
:SetContext(pageName)
:SetText(pageName)
:SetScript("OnClick", private.NavButtonOnClick)
)
content:AddPath(pageName, pageName == defaultPage)
if private.childPages[pageName] then
for _, childPageName in ipairs(private.childPages[pageName]) do
local path = pageName..PAGE_PATH_SEP..childPageName
navFrame:AddChild(UIElements.New("Button", path)
:SetHeight(20)
:SetMargin(9, 0, 8, 0)
:SetFont("BODY_BODY3_MEDIUM")
:SetJustifyH("LEFT")
:SetContext(path)
:SetText(childPageName)
:SetScript("OnClick", private.NavButtonOnClick)
)
content:AddPath(path, path == defaultPage)
end
end
end
-- make all the navigation align to the top
navFrame:AddChild(UIElements.New("Spacer", "spacer"))
private.UpdateNavFrame(navFrame, defaultPage)
private.contextPath = L["Inventory"]
return frame
end
function private.ContentNavCallback(self, path)
if path == "itemDetail" then
return private.GetItemDetail()
else
return private.callback[path]()
end
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.NavButtonOnClick(button)
local path = button:GetContext()
if private.contextPath == path then
return
end
if private.childPages[path] then
-- select the first child
path = path..PAGE_PATH_SEP..private.childPages[path][1]
end
local ledgerFrame = button:GetParentElement():GetParentElement()
local contentFrame = ledgerFrame:GetElement("contentFrame")
local navFrame = ledgerFrame:GetElement("navigation")
private.UpdateNavFrame(navFrame, path)
navFrame:Draw()
contentFrame:GetElement("content"):SetPath(path, private.contextPath ~= path)
private.contextPath = path
end
function private.NavButtonOnHide(button)
private.contextPath = nil
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.UpdateNavFrame(navFrame, selectedPath)
local selectedPage = strsplit(PAGE_PATH_SEP, selectedPath)
for _, pageName in ipairs(private.pages) do
navFrame:GetElement(pageName):SetTextColor(pageName == selectedPage and "TEXT" or "ACTIVE_BG_ALT")
if private.childPages[pageName] then
for _, childPageName in ipairs(private.childPages[pageName]) do
local path = pageName..PAGE_PATH_SEP..childPageName
if pageName == selectedPage then
navFrame:GetElement(path)
:SetTextColor(path == selectedPath and "INDICATOR" or "TEXT")
:Show()
else
navFrame:GetElement(path):Hide()
end
end
end
end
end
function private.GetItemDetail()
local query = TSM.Accounting.Transactions.CreateQuery()
:Equal("itemString", private.contextItemString)
:OrderBy("time", false)
local topPlayersQuantity = TempTable.Acquire()
local topPlayers = TempTable.Acquire()
for _, row in query:Iterator() do
local recordType, otherPlayer, quantity = row:GetFields("type", "otherPlayer", "quantity")
if recordType == private.itemDetailType then
if not topPlayersQuantity[otherPlayer] then
topPlayersQuantity[otherPlayer] = 0
tinsert(topPlayers, otherPlayer)
end
topPlayersQuantity[otherPlayer] = topPlayersQuantity[otherPlayer] + quantity
end
end
Table.SortWithValueLookup(topPlayers, topPlayersQuantity, true)
local numTopPlayers = min(#topPlayers, NUM_TOP_PLAYERS)
local topPlayersText = ""
if numTopPlayers > 0 then
for i = 1, numTopPlayers do
local player = topPlayers[i]
local quantity = topPlayersQuantity[player]
topPlayers[i] = player..Theme.GetColor("INDICATOR_ALT"):ColorText(" (" .. quantity .. ")")
end
topPlayersText = table.concat(topPlayers, ", ", 1, numTopPlayers)
else
topPlayersText = L["None"]
end
TempTable.Release(topPlayers)
TempTable.Release(topPlayersQuantity)
return UIElements.New("Frame", "content")
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Frame", "top")
:SetLayout("VERTICAL")
:SetPadding(8)
:SetBackgroundColor("PRIMARY_BG_ALT")
:AddChild(UIElements.New("Frame", "header")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("ActionButton", "button")
:SetWidth(64)
:SetIcon("iconPack.14x14/Chevron/Right@180")
:SetText(BACK)
:SetScript("OnClick", private.ItemDetailBackButtonOnClick)
)
:AddChild(UIElements.New("Button", "icon")
:SetSize(24, 24)
:SetMargin(14, 8, 0, 0)
:SetBackground(ItemInfo.GetTexture(private.contextItemString))
:SetTooltip(private.contextItemString)
)
:AddChild(UIElements.New("Text", "itemName")
:SetFont("ITEM_BODY1")
:SetText(TSM.UI.GetColoredItemName(private.contextItemString))
)
)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("VERTICAL")
:SetMargin(0, 0, 8, 0)
:SetPadding(12, 12, 8, 10)
:SetBackgroundColor("PRIMARY_BG_ALT", true)
:SetBorderColor("ACTIVE_BG")
:AddChild(UIElements.New("Frame", "heading")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:AddChild(UIElements.New("Button", "saleBtn")
:SetWidth("AUTO")
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY2_BOLD")
:SetTextColor(private.itemDetailType == "sale" and "INDICATOR" or "ACTIVE_BG_ALT")
:SetContext("sale")
:SetText(L["Sale Data"])
:SetScript("OnClick", private.ItemDetailTabOnClick)
)
:AddChild(UIElements.New("Button", "buyBtn")
:SetWidth("AUTO")
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY2_BOLD")
:SetTextColor(private.itemDetailType == "buy" and "INDICATOR" or "ACTIVE_BG_ALT")
:SetContext("buy")
:SetText(L["Purchase Data"])
:SetScript("OnClick", private.ItemDetailTabOnClick)
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "total")
:SetWidth(120)
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY3_MEDIUM")
:SetJustifyH("RIGHT")
:SetText(L["Total"])
)
:AddChild(UIElements.New("Text", "last7")
:SetWidth(120)
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY3_MEDIUM")
:SetJustifyH("RIGHT")
:SetText(L["Last 7 Days"])
)
:AddChild(UIElements.New("Text", "last30")
:SetWidth(120)
:SetFont("BODY_BODY3_MEDIUM")
:SetJustifyH("RIGHT")
:SetText(L["Last 30 Days"])
)
)
:AddChild(UIElements.New("Frame", "quantity")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 8, 0)
:AddChild(UIElements.New("Text", "label")
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY3")
:SetTextColor("TEXT_ALT")
:SetText(private.itemDetailType == "sale" and L["Quantity Sold:"] or L["Quantity Purchased:"])
)
:AddChild(UIElements.New("Text", "total")
:SetWidth(120)
:SetMargin(0, 8, 0, 0)
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetText(TSM.Accounting.Transactions.GetQuantity(private.contextItemString, nil, private.itemDetailType))
)
:AddChild(UIElements.New("Text", "last7")
:SetWidth(120)
:SetMargin(0, 8, 0, 0)
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetText(TSM.Accounting.Transactions.GetQuantity(private.contextItemString, SECONDS_PER_DAY * 7, private.itemDetailType))
)
:AddChild(UIElements.New("Text", "last30")
:SetWidth(120)
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetText(TSM.Accounting.Transactions.GetQuantity(private.contextItemString, SECONDS_PER_DAY * 30, private.itemDetailType))
)
)
:AddChild(UIElements.New("Frame", "avgPrice")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 8, 0)
:AddChild(UIElements.New("Text", "label")
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY3")
:SetTextColor("TEXT_ALT")
:SetText(L["Average Prices:"])
)
:AddChild(UIElements.New("Text", "total")
:SetWidth(120)
:SetMargin(0, 8, 0, 0)
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetText(Money.ToString(TSM.Accounting.Transactions.GetAveragePrice(private.contextItemString, nil, private.itemDetailType)))
)
:AddChild(UIElements.New("Text", "last7")
:SetWidth(120)
:SetMargin(0, 8, 0, 0)
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetText(Money.ToString(TSM.Accounting.Transactions.GetAveragePrice(private.contextItemString, SECONDS_PER_DAY * 7, private.itemDetailType)))
)
:AddChild(UIElements.New("Text", "last30")
:SetWidth(120)
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetText(Money.ToString(TSM.Accounting.Transactions.GetAveragePrice(private.contextItemString, SECONDS_PER_DAY * 30, private.itemDetailType)))
)
)
:AddChild(UIElements.New("Frame", "totalPrice")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 8, 0)
:AddChild(UIElements.New("Text", "label")
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY3")
:SetTextColor("TEXT_ALT")
:SetText(L["Total Prices:"])
)
:AddChild(UIElements.New("Text", "total")
:SetWidth(120)
:SetMargin(0, 8, 0, 0)
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetText(Money.ToString(TSM.Accounting.Transactions.GetTotalPrice(private.contextItemString, nil, private.itemDetailType)))
)
:AddChild(UIElements.New("Text", "last7")
:SetWidth(120)
:SetMargin(0, 8, 0, 0)
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetText(Money.ToString(TSM.Accounting.Transactions.GetTotalPrice(private.contextItemString, SECONDS_PER_DAY * 7, private.itemDetailType)))
)
:AddChild(UIElements.New("Text", "last30")
:SetWidth(120)
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetText(Money.ToString(TSM.Accounting.Transactions.GetTotalPrice(private.contextItemString, SECONDS_PER_DAY * 30, private.itemDetailType)))
)
)
:AddChild(UIElements.New("Frame", "top")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 8, 0)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY3")
:SetTextColor("TEXT_ALT")
:SetText(private.itemDetailType == "sale" and L["Top Buyers"]..":" or L["Top Sellers"]..":")
)
:AddChild(UIElements.New("Text", "value")
:SetFont("BODY_BODY3")
:SetText(topPlayersText)
)
)
)
)
:AddChild(UIElements.New("QueryScrollingTable", "scrollingTable")
:SetSettingsContext(private.settings, "ledgerDetailScrollingTable")
:GetScrollingTableInfo()
:NewColumn("activityType")
:SetTitle(L["Activity Type"])
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("type", private.TableGetActivityTypeText)
:Commit()
:NewColumn("source")
:SetTitle(L["Source"])
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("source")
:Commit()
:NewColumn("buyerSeller")
:SetTitle(L["Buyer/Seller"])
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("otherPlayer")
:Commit()
:NewColumn("qty")
:SetTitle(L["Qty"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("quantity")
:Commit()
:NewColumn("perItem")
:SetTitle(L["Per Item"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo(nil, private.TableGetPerItemText)
:Commit()
:NewColumn("totalPrice")
:SetTitle(L["Total Price"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo(nil, private.TableGetTotalPriceText)
:Commit()
:NewColumn("time")
:SetTitle(L["Time"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("time", private.TableGetTimeframeText)
:Commit()
:Commit()
:SetQuery(query)
:SetAutoReleaseQuery(true)
:SetSelectionDisabled(true)
:SetScript("OnRowClick", private.ItemDetailScrollingTableOnRowClick)
)
end
function private.ItemDetailBackButtonOnClick(button)
button:GetParentElement():GetParentElement():GetParentElement():GetParentElement():SetPath(private.contextPath, true)
end
function private.ItemDetailTabOnClick(button)
private.itemDetailType = button:GetContext()
button:GetParentElement():GetParentElement():GetParentElement():GetParentElement():GetParentElement():ReloadContent()
end
function private.ItemDetailScrollingTableOnRowClick(scrollingTable, row, button)
if button ~= "RightButton" then
return
elseif not TSM.Accounting.Transactions.CanDeleteByUUID(row:GetUUID()) then
Log.PrintUser(L["This record belongs to another account and can only be deleted on that account."])
return
end
local subtitle = nil
local recordType, itemString, quantity, otherPlayer, price = row:GetFields("type", "itemString", "quantity", "otherPlayer", "price")
local name = TSM.UI.GetColoredItemName(itemString) or "?"
local amount = Money.ToString(price * quantity)
if recordType == "sale" then
subtitle = format(L["Sold %d of %s to %s for %s"], quantity, name, otherPlayer, amount)
elseif recordType == "buy" then
subtitle = format(L["Bought %d of %s from %s for %s"], quantity, name, otherPlayer, amount)
else
error("Unexpected Type: "..tostring(recordType))
end
scrollingTable:GetBaseElement():ShowConfirmationDialog(L["Delete Record?"], subtitle, private.DeleteRecordConfirmed, row:GetUUID())
end
function private.DeleteRecordConfirmed(uuid)
TSM.Accounting.Transactions.RemoveRowByUUID(uuid)
end
-- ============================================================================
-- Scrolling Table Helper Functions
-- ============================================================================
function private.TableGetActivityTypeText(recordType)
if recordType == "sale" then
return L["Sale"]
elseif recordType == "buy" then
return L["Buy"]
else
error("Unexpected Type: "..tostring(recordType))
end
end
function private.TableGetTimeframeText(timestamp)
return SecondsToTime(time() - timestamp)
end
function private.TableGetTotalPriceText(row)
return Money.ToString(row:GetField("price") * row:GetField("quantity"))
end
function private.TableGetPerItemText(row)
return Money.ToString(row:GetField("price"))
end

View File

@@ -0,0 +1,23 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Expenses = TSM.MainUI.Ledger:NewPackage("Expenses")
local L = TSM.Include("Locale").GetTable()
-- ============================================================================
-- Module Functions
-- ============================================================================
function Expenses.OnInitialize()
TSM.MainUI.Ledger.RegisterPage(L["Expenses"])
end
function Expenses.RegisterPage(name, callback)
TSM.MainUI.Ledger.RegisterChildPage(L["Expenses"], name, callback)
end

View File

@@ -0,0 +1,23 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local FailedAuctions = TSM.MainUI.Ledger:NewPackage("FailedAuctions")
local L = TSM.Include("Locale").GetTable()
-- ============================================================================
-- Module Functions
-- ============================================================================
function FailedAuctions.OnInitialize()
TSM.MainUI.Ledger.RegisterPage(L["Failed Auctions"])
end
function FailedAuctions.RegisterPage(name, callback)
TSM.MainUI.Ledger.RegisterChildPage(L["Failed Auctions"], name, callback)
end

View File

@@ -0,0 +1,329 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Inventory = TSM.MainUI.Ledger:NewPackage("Inventory")
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
local Money = TSM.Include("Util.Money")
local String = TSM.Include("Util.String")
local Math = TSM.Include("Util.Math")
local Database = TSM.Include("Util.Database")
local ItemInfo = TSM.Include("Service.ItemInfo")
local CustomPrice = TSM.Include("Service.CustomPrice")
local BagTracking = TSM.Include("Service.BagTracking")
local GuildTracking = TSM.Include("Service.GuildTracking")
local AuctionTracking = TSM.Include("Service.AuctionTracking")
local MailTracking = TSM.Include("Service.MailTracking")
local AltTracking = TSM.Include("Service.AltTracking")
local InventoryService = TSM.Include("Service.Inventory")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
local private = {
settings = nil,
db = nil,
query = nil,
searchFilter = "",
groupFilter = {},
valuePriceSource = "dbmarket", -- luacheck: ignore 1005 - hidden modify via SetSettingInfo()
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Inventory.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "mainUIContext", "ledgerInventoryScrollingTable")
TSM.MainUI.Ledger.RegisterPage(L["Inventory"], private.DrawInventoryPage)
end
function Inventory.OnEnable()
private.db = Database.NewSchema("LEDGER_INVENTORY")
:AddUniqueStringField("itemString")
:Commit()
private.query = private.db:NewQuery()
:VirtualField("bagQuantity", "number", BagTracking.GetBagsQuantityByBaseItemString, "itemString")
:VirtualField("guildQuantity", "number", private.GuildQuantityVirtualField, "itemString")
:VirtualField("auctionQuantity", "number", AuctionTracking.GetQuantityByBaseItemString, "itemString")
:VirtualField("mailQuantity", "number", MailTracking.GetQuantityByBaseItemString, "itemString")
:VirtualField("altQuantity", "number", AltTracking.GetQuantityByBaseItemString, "itemString")
:VirtualField("totalQuantity", "number", private.TotalQuantityVirtualField)
:VirtualField("totalValue", "number", private.TotalValueVirtualField)
:VirtualField("totalBankQuantity", "number", private.GetTotalBankQuantity)
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
:LeftJoin(TSM.Groups.GetItemDBForJoin(), "itemString")
:OrderBy("name", true)
end
-- ============================================================================
-- Inventory UI
-- ============================================================================
function private.DrawInventoryPage()
TSM.UI.AnalyticsRecordPathChange("main", "ledger", "inventory")
local items = TempTable.Acquire()
for _, itemString in BagTracking.BaseItemIterator() do
items[itemString] = true
end
for _, itemString in GuildTracking.BaseItemIterator() do
items[itemString] = true
end
for _, itemString in AuctionTracking.BaseItemIterator() do
items[itemString] = true
end
for _, itemString in MailTracking.BaseItemIterator() do
items[itemString] = true
end
for _, itemString in AltTracking.BaseItemIterator() do
items[itemString] = true
end
private.db:TruncateAndBulkInsertStart()
for itemString in pairs(items) do
private.db:BulkInsertNewRow(itemString)
end
private.db:BulkInsertEnd()
TempTable.Release(items)
private.UpdateQuery()
return UIElements.New("Frame", "content")
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Frame", "row1")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(8)
:AddChild(UIElements.New("Input", "filter")
:SetMargin(0, 8, 0, 0)
:SetIconTexture("iconPack.18x18/Search")
:SetClearButtonEnabled(true)
:AllowItemInsert()
:SetHintText(L["Filter by keyword"])
:SetValue(private.searchFilter)
:SetScript("OnValueChanged", private.SearchFilterChanged)
)
:AddChild(UIElements.New("GroupSelector", "group")
:SetWidth(240)
:SetHintText(L["Filter by groups"])
:SetScript("OnSelectionChanged", private.GroupFilterChanged)
)
)
:AddChild(UIElements.New("Frame", "row2")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(8, 8, 0, 8)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY3")
:SetText(L["Value Price Source"])
)
:AddChild(UIElements.New("Input", "input")
:SetMargin(4, 8, 0, 0)
:SetBackgroundColor("PRIMARY_BG_ALT")
:SetBorderColor("ACTIVE_BG")
:SetFont("TABLE_TABLE1")
:SetValidateFunc("CUSTOM_PRICE")
:SetSettingInfo(private, "valuePriceSource")
:SetScript("OnValueChanged", private.FilterChangedCommon)
)
:AddChild(UIElements.New("Frame", "value")
:SetLayout("HORIZONTAL")
:SetWidth(240)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY3")
:SetMargin(0, 4, 0, 0)
:SetText(L["Total Value"]..":")
)
:AddChild(UIElements.New("Text", "value")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetText(Money.ToString(private.GetTotalValue()))
)
)
)
:AddChild(UIElements.New("Frame", "accountingScrollingTableFrame")
:SetLayout("VERTICAL")
:AddChild(UIElements.New("QueryScrollingTable", "scrollingTable")
:SetSettingsContext(private.settings, "ledgerInventoryScrollingTable")
:GetScrollingTableInfo()
:NewColumn("item")
:SetTitle(L["Item"])
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("itemString", TSM.UI.GetColoredItemName)
:SetTooltipInfo("itemString")
:SetSortInfo("name")
:DisableHiding()
:Commit()
:NewColumn("totalItems")
:SetTitle(L["Total"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("totalQuantity")
:SetSortInfo("totalQuantity")
:Commit()
:NewColumn("bags")
:SetTitle(L["Bags"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("bagQuantity")
:SetSortInfo("bagQuantity")
:Commit()
:NewColumn("banks")
:SetTitle(L["Banks"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("totalBankQuantity")
:SetSortInfo("totalBankQuantity")
:Commit()
:NewColumn("mail")
:SetTitle(L["Mail"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("mailQuantity")
:SetSortInfo("mailQuantity")
:Commit()
:NewColumn("alts")
:SetTitle(L["Alts"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("altQuantity")
:SetSortInfo("altQuantity")
:Commit()
:NewColumn("guildVault")
:SetTitle(L["GVault"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("guildQuantity")
:SetSortInfo("guildQuantity")
:Commit()
:NewColumn("auctionHouse")
:SetTitle(L["AH"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("auctionQuantity")
:SetSortInfo("auctionQuantity")
:Commit()
:NewColumn("totalValue")
:SetTitle(L["Value"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("totalValue", private.TableGetTotalValueText)
:SetSortInfo("totalValue")
:Commit()
:Commit()
:SetSelectionDisabled(true)
:SetQuery(private.query)
)
)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.FilterChangedCommon(element)
private.UpdateQuery()
element:GetElement("__parent.__parent.accountingScrollingTableFrame.scrollingTable")
:SetQuery(private.query, true)
element:GetElement("__parent.__parent.row2.value.value")
:SetText(Money.ToString(private.GetTotalValue()))
:Draw()
end
function private.SearchFilterChanged(input)
private.searchFilter = input:GetValue()
private.FilterChangedCommon(input)
end
function private.GroupFilterChanged(groupSelector)
wipe(private.groupFilter)
for groupPath in groupSelector:SelectedGroupIterator() do
private.groupFilter[groupPath] = true
end
private.FilterChangedCommon(groupSelector)
end
-- ============================================================================
-- Scrolling Table Helper Functions
-- ============================================================================
function private.TableGetTotalValueText(totalValue)
return Math.IsNan(totalValue) and "" or Money.ToString(totalValue)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GuildQuantityVirtualField(itemString)
local totalNum = 0
for guildName in pairs(TSM.db.factionrealm.internalData.guildVaults) do
local guildQuantity = InventoryService.GetGuildQuantity(itemString, guildName)
totalNum = totalNum + guildQuantity
end
return totalNum
end
function private.TotalQuantityVirtualField(row)
local bagQuantity, totalBankQuantity, guildQuantity, auctionQuantity, mailQuantity, altQuantity = row:GetFields("bagQuantity", "totalBankQuantity", "guildQuantity", "auctionQuantity", "mailQuantity", "altQuantity")
return bagQuantity + totalBankQuantity + guildQuantity + auctionQuantity + mailQuantity + altQuantity
end
function private.TotalValueVirtualField(row)
local itemString, totalQuantity = row:GetFields("itemString", "totalQuantity")
local price = CustomPrice.GetValue(private.valuePriceSource, itemString)
if not price then
return Math.GetNan()
end
return price * totalQuantity
end
function private.GetTotalBankQuantity(row)
local itemString = row:GetField("itemString")
local bankQuantity = BagTracking.GetBankQuantityByBaseItemString(itemString)
local reagentBankQuantity = BagTracking.GetReagentBankQuantityByBaseItemString(itemString)
return bankQuantity + reagentBankQuantity
end
function private.GetTotalValue()
-- can't lookup the value of items while the query is iteratoring, so grab the list of items first
local itemQuantities = TempTable.Acquire()
for _, row in private.query:Iterator() do
local itemString, total = row:GetFields("itemString", "totalQuantity")
itemQuantities[itemString] = total
end
local totalValue = 0
for itemString, total in pairs(itemQuantities) do
local price = CustomPrice.GetValue(private.valuePriceSource, itemString)
if price then
totalValue = totalValue + price * total
end
end
TempTable.Release(itemQuantities)
return totalValue
end
function private.UpdateQuery()
private.query:ResetFilters()
if private.searchFilter ~= "" then
private.query:Matches("name", String.Escape(private.searchFilter))
end
if next(private.groupFilter) then
private.query:InTable("groupPath", private.groupFilter)
end
end

View File

@@ -0,0 +1,23 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Revenue = TSM.MainUI.Ledger:NewPackage("Revenue")
local L = TSM.Include("Locale").GetTable()
-- ============================================================================
-- Module Functions
-- ============================================================================
function Revenue.OnInitialize()
TSM.MainUI.Ledger.RegisterPage(L["Revenue"])
end
function Revenue.RegisterPage(name, callback)
TSM.MainUI.Ledger.RegisterChildPage(L["Revenue"], name, callback)
end

View File

@@ -0,0 +1,299 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Resale = TSM.MainUI.Ledger.Revenue:NewPackage("Resale")
local L = TSM.Include("Locale").GetTable()
local Table = TSM.Include("Util.Table")
local Money = TSM.Include("Util.Money")
local Theme = TSM.Include("Util.Theme")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
local SECONDS_PER_DAY = 24 * 60 * 60
local private = {
settings = nil,
summaryQuery = nil,
characters = {},
characterFilter = {},
typeFilter = {},
rarityList = {},
rarityFilter = {},
groupFilter = {},
searchFilter = "",
timeFrameFilter = 30 * SECONDS_PER_DAY
}
local TYPE_LIST = { L["Auction"], COD, TRADE, L["Vendor"] }
local TYPE_KEYS = { "Auction", "COD", "Trade", "Vendor" }
do
for _, key in ipairs(TYPE_KEYS) do
private.typeFilter[key] = true
end
for i = 1, 4 do
tinsert(private.rarityList, _G[format("ITEM_QUALITY%d_DESC", i)])
private.rarityFilter[i] = true
end
end
local TIME_LIST = { L["All Time"], L["Last 3 Days"], L["Last 7 Days"], L["Last 14 Days"], L["Last 30 Days"], L["Last 60 Days"] }
local TIME_KEYS = { 0, 3 * SECONDS_PER_DAY, 7 * SECONDS_PER_DAY, 14 * SECONDS_PER_DAY, 30 * SECONDS_PER_DAY, 60 * SECONDS_PER_DAY }
-- ============================================================================
-- Module Functions
-- ============================================================================
function Resale.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "mainUIContext", "ledgerResaleScrollingTable")
TSM.MainUI.Ledger.Revenue.RegisterPage(L["Resale"], private.DrawResalePage)
end
-- ============================================================================
-- Resale UI
-- ============================================================================
function private.DrawResalePage()
TSM.UI.AnalyticsRecordPathChange("main", "ledger", "revenue", "resale")
wipe(private.characters)
TSM.Accounting.Transactions.GetCharacters(private.characters)
for _, character in ipairs(private.characters) do
private.characterFilter[character] = true
end
private.summaryQuery = private.summaryQuery or TSM.Accounting.Transactions.CreateSummaryQuery()
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
:OrderBy("name", true)
private.UpdateQuery()
local totalProfit = 0
local numItems = 0
for _, row in private.summaryQuery:Iterator() do
totalProfit = totalProfit + row:GetField("totalProfit")
numItems = numItems + min(row:GetFields("sold", "bought"))
end
return UIElements.New("Frame", "content")
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Frame", "row1")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(8)
:AddChild(UIElements.New("Input", "filter")
:SetMargin(0, 8, 0, 0)
:SetIconTexture("iconPack.18x18/Search")
:SetClearButtonEnabled(true)
:AllowItemInsert()
:SetHintText(L["Filter by keyword"])
:SetValue(private.searchFilter)
:SetScript("OnValueChanged", private.SearchFilterChanged)
)
:AddChild(UIElements.New("GroupSelector", "group")
:SetWidth(240)
:SetHintText(L["Filter by groups"])
:SetScript("OnSelectionChanged", private.GroupFilterChanged)
)
)
:AddChild(UIElements.New("Frame", "row2")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(8, 8, 0, 8)
:AddChild(UIElements.New("MultiselectionDropdown", "type")
:SetMargin(0, 8, 0, 0)
:SetItems(TYPE_LIST, TYPE_KEYS)
:SetSettingInfo(private, "typeFilter")
:SetSelectionText(L["No Types"], L["%d Types"], L["All Types"])
:SetScript("OnSelectionChanged", private.FilterChangedCommon)
)
:AddChild(UIElements.New("MultiselectionDropdown", "rarity")
:SetMargin(0, 8, 0, 0)
:SetItems(private.rarityList)
:SetSettingInfo(private, "rarityFilter")
:SetSelectionText(L["No Rarities"], L["%d Rarities"], L["All Rarities"])
:SetScript("OnSelectionChanged", private.FilterChangedCommon)
)
:AddChild(UIElements.New("MultiselectionDropdown", "character")
:SetMargin(0, 8, 0, 0)
:SetItems(private.characters, private.characters)
:SetSettingInfo(private, "characterFilter")
:SetSelectionText(L["No Characters"], L["%d Characters"], L["All Characters"])
:SetScript("OnSelectionChanged", private.FilterChangedCommon)
)
:AddChild(UIElements.New("SelectionDropdown", "time")
:SetItems(TIME_LIST, TIME_KEYS)
:SetSelectedItemByKey(private.timeFrameFilter)
:SetSettingInfo(private, "timeFrameFilter")
:SetScript("OnSelectionChanged", private.FilterChangedCommon)
)
)
:AddChild(UIElements.New("QueryScrollingTable", "scrollingTable")
:SetSettingsContext(private.settings, "ledgerResaleScrollingTable")
:GetScrollingTableInfo()
:NewColumn("item")
:SetTitle(L["Item"])
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo("itemString", TSM.UI.GetColoredItemName)
:SetSortInfo("name")
:SetTooltipInfo("itemString")
:DisableHiding()
:Commit()
:NewColumn("bought")
:SetTitle(L["Bought"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("bought")
:SetSortInfo("bought")
:Commit()
:NewColumn("avgBuyPrice")
:SetTitle(L["Avg Buy Price"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("avgBuyPrice", private.GetMoneyText)
:SetSortInfo("avgBuyPrice")
:Commit()
:NewColumn("sold")
:SetTitle(L["Sold"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("sold")
:SetSortInfo("sold")
:Commit()
:NewColumn("avgSellPrice")
:SetTitle(L["Avg Sell Price"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("avgSellPrice", private.GetMoneyText)
:SetSortInfo("avgSellPrice")
:Commit()
:NewColumn("avgProfit")
:SetTitle(L["Avg Profit"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("avgProfit", private.GetColoredMoneyText)
:SetSortInfo("avgProfit")
:Commit()
:NewColumn("totalProfit")
:SetTitle(L["Total Profit"])
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("totalProfit", private.GetColoredMoneyText)
:SetSortInfo("totalProfit")
:Commit()
:NewColumn("profitPct")
:SetTitle("%")
:SetFont("ITEM_BODY3")
:SetJustifyH("RIGHT")
:SetTextInfo("profitPct", private.GetPctText)
:SetSortInfo("profitPct")
:Commit()
:Commit()
:SetQuery(private.summaryQuery)
:SetScript("OnRowClick", private.TableSelectionChanged)
)
:AddChild(UIElements.New("Texture", "line")
:SetHeight(2)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Frame", "footer")
:SetLayout("HORIZONTAL")
:SetHeight(40)
:SetPadding(8)
:SetBackgroundColor("PRIMARY_BG")
:AddChild(UIElements.New("Text", "num")
:SetWidth("AUTO")
:SetFont("BODY_BODY2_MEDIUM")
:SetText(format(L["%s Items Resold"], Theme.GetColor("INDICATOR"):ColorText(FormatLargeNumber(numItems))))
)
:AddChild(UIElements.New("Texture", "line")
:SetMargin(4, 8, 0, 0)
:SetWidth(2)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Text", "profit")
:SetWidth("AUTO")
:SetFont("BODY_BODY2_MEDIUM")
:SetText(format(L["%s Total Profit"], Money.ToString(totalProfit)))
)
:AddChild(UIElements.New("Spacer", "spacer"))
)
end
-- ============================================================================
-- Scrolling Table Helper Functions
-- ============================================================================
function private.GetMoneyText(value)
return Money.ToString(value)
end
function private.GetColoredMoneyText(value)
return Money.ToString(value, Theme.GetFeedbackColor(value >= 0 and "GREEN" or "RED"):GetTextColorPrefix())
end
function private.GetPctText(value)
return Theme.GetFeedbackColor(value >= 0 and "GREEN" or "RED"):ColorText(value.."%")
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.FilterChangedCommon(dropdown)
private.UpdateQuery()
local totalProfit = 0
local numItems = 0
for _, row in private.summaryQuery:Iterator() do
totalProfit = totalProfit + row:GetField("totalProfit")
numItems = numItems + min(row:GetFields("sold", "bought"))
end
dropdown:GetElement("__parent.__parent.scrollingTable"):UpdateData(true)
local footer = dropdown:GetElement("__parent.__parent.footer")
footer:GetElement("num"):SetText(format(L["%s Items Resold"], Theme.GetColor("INDICATOR"):ColorText(FormatLargeNumber(numItems))))
footer:GetElement("profit"):SetText(format(L["%s Total Profit"], Money.ToString(totalProfit)))
footer:Draw()
end
function private.SearchFilterChanged(input)
private.searchFilter = input:GetValue()
private.FilterChangedCommon(input)
end
function private.GroupFilterChanged(groupSelector)
wipe(private.groupFilter)
for groupPath in groupSelector:SelectedGroupIterator() do
private.groupFilter[groupPath] = true
end
private.FilterChangedCommon(groupSelector)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.UpdateQuery()
private.summaryQuery:ResetFilters()
local groupFilter = next(private.groupFilter) and private.groupFilter or nil
local searchFilter = private.searchFilter ~= "" and private.searchFilter or nil
local typeFilter = Table.Count(private.typeFilter) ~= #TYPE_KEYS and private.typeFilter or nil
local characterFilter = Table.Count(private.characterFilter) ~= #private.characters and private.characterFilter or nil
local minTime = private.timeFrameFilter ~= 0 and (time() - private.timeFrameFilter) or nil
TSM.Accounting.Transactions.UpdateSummaryData(groupFilter, searchFilter, typeFilter, characterFilter, minTime)
if Table.Count(private.rarityFilter) ~= #private.rarityList then
private.summaryQuery:InTable("quality", private.rarityFilter)
end
end
function private.TableSelectionChanged(scrollingTable, row)
TSM.MainUI.Ledger.ShowItemDetail(scrollingTable:GetParentElement():GetParentElement(), row:GetField("itemString"), "sale")
end

View File

@@ -0,0 +1,360 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Auctioning = TSM.MainUI.Operations:NewPackage("Auctioning")
local L = TSM.Include("Locale").GetTable()
local String = TSM.Include("Util.String")
local Money = TSM.Include("Util.Money")
local Vararg = TSM.Include("Util.Vararg")
local CustomPrice = TSM.Include("Service.CustomPrice")
local UIElements = TSM.Include("UI.UIElements")
local private = {
currentOperationName = nil,
currentTab = nil,
}
local IGNORE_DURATION_OPTIONS = {
L["None"],
AUCTION_TIME_LEFT1.." ("..AUCTION_TIME_LEFT1_DETAIL..")",
AUCTION_TIME_LEFT2.." ("..AUCTION_TIME_LEFT2_DETAIL..")",
AUCTION_TIME_LEFT3.." ("..AUCTION_TIME_LEFT3_DETAIL..")",
}
local BELOW_MIN_ITEMS = { L["Don't Post Items"], L["Post at Minimum Price"], L["Post at Maximum Price"], L["Post at Normal Price"], L["Ignore Auctions Below Min"] }
local BELOW_MIN_KEYS = { "none", "minPrice", "maxPrice", "normalPrice", "ignore" }
local ABOVE_MAX_ITEMS = { L["Don't Post Items"], L["Post at Minimum Price"], L["Post at Maximum Price"], L["Post at Normal Price"] }
local ABOVE_MAX_KEYS = { "none", "minPrice", "maxPrice", "normalPrice" }
local BAD_PRICE_SOURCES = { auctioningopmin = true, auctioningopmax = true, auctioningopnormal = true }
-- ============================================================================
-- Module Functions
-- ============================================================================
function Auctioning.OnInitialize()
TSM.MainUI.Operations.RegisterModule("Auctioning", private.GetAuctioningOperationSettings)
end
-- ============================================================================
-- Auctioning Operation Settings UI
-- ============================================================================
function private.GetAuctioningOperationSettings(operationName)
TSM.UI.AnalyticsRecordPathChange("main", "operations", "auctioning")
private.currentOperationName = operationName
private.currentTab = private.currentTab or L["Details"]
return UIElements.New("TabGroup", "tabs")
:SetMargin(0, 0, 8, 0)
:SetNavCallback(private.GetAuctioningSettings)
:AddPath(L["Details"])
:AddPath(L["Posting"])
:AddPath(L["Canceling"])
:SetPath(private.currentTab)
end
function private.GetDetailsSettings()
TSM.UI.AnalyticsRecordPathChange("main", "operations", "auctioning", "details")
local operation = TSM.Operations.GetSettings("Auctioning", private.currentOperationName)
return UIElements.New("ScrollFrame", "settings")
:SetPadding(8, 8, 8, 0)
:SetBackgroundColor("PRIMARY_BG")
:AddChild(TSM.MainUI.Operations.CreateExpandableSection("Auctioning", "generalOptions", L["General Options"], L["Adjust some general settings."])
:AddChild(UIElements.New("Frame", "content")
:SetLayout("VERTICAL")
:AddChildrenWithFunction(private.AddMaxStackSizeSetting)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("ignoreLowDuration", L["Ignore auctions by duration"])
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 16)
:AddChild(UIElements.New("SelectionDropdown", "dropdown")
:SetHeight(24)
:SetDisabled(TSM.Operations.HasRelationship("Auctioning", private.currentOperationName, "ignoreLowDuration"))
:SetItems(IGNORE_DURATION_OPTIONS)
:SetSelectedItemByKey(operation.ignoreLowDuration + 1, true)
:SetScript("OnSelectionChanged", private.IgnoreLowDurationOnSelectionChanged)
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("blacklist", L["Blacklisted players"])
:SetLayout("VERTICAL")
:SetHeight(48)
:AddChild(UIElements.New("Input", "input")
:SetHeight(24)
:SetBackgroundColor("ACTIVE_BG")
:SetHintText(L["Enter player name"])
:SetDisabled(TSM.Operations.HasRelationship("Auctioning", private.currentOperationName, "blacklist"))
:SetScript("OnEnterPressed", private.BlacklistInputOnEnterPressed)
)
)
:AddChildrenWithFunction(private.AddBlacklistPlayers)
)
)
:AddChild(TSM.MainUI.Operations.GetOperationManagementElements("Auctioning", private.currentOperationName))
end
function private.AddMaxStackSizeSetting(frame)
if TSM.IsWowClassic() then
frame:AddChild(private.CreateToggleLine("matchStackSize", L["Match stack size"]))
end
end
function private.GetPostingSettings()
TSM.UI.AnalyticsRecordPathChange("main", "operations", "auctioning", "posting")
local operation = TSM.Operations.GetSettings("Auctioning", private.currentOperationName)
return UIElements.New("ScrollFrame", "settings")
:SetPadding(8, 8, 8, 0)
:SetBackgroundColor("PRIMARY_BG")
:AddChild(TSM.MainUI.Operations.CreateExpandableSection("Auctioning", "postingSettings", L["Posting Options"], L["Adjust the settings below to set how groups attached to this operation will be auctioned."])
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("duration", L["Auction duration"])
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("SelectionDropdown", "dropdown")
:SetHeight(24)
:SetDisabled(TSM.Operations.HasRelationship("Auctioning", private.currentOperationName, "duration"))
:SetItems(TSM.CONST.AUCTION_DURATIONS)
:SetSelectedItemByKey(operation.duration)
:SetScript("OnSelectionChanged", private.SetAuctioningDuration)
)
)
:AddChild(private.CreateInputLine("postCap", L["Post cap"], false, true))
:AddChildrenWithFunction(private.AddStackSizeSettings)
:AddChild(private.CreateInputLine("keepQuantity", L["Amount kept in bags"], false, true))
:AddChild(private.CreateInputLine("maxExpires", L["Don't post after this many expires"]))
)
:AddChild(TSM.MainUI.Operations.CreateExpandableSection("Auctioning", "priceSettings", L["Posting Price"], L["Adjust the settings below to set how groups attached to this operation will be priced."])
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("bidPercent", L["Set bid as percentage of buyout"])
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Input", "input")
:SetMargin(0, 8, 0, 0)
:SetBackgroundColor("ACTIVE_BG")
:SetValidateFunc(private.BidPercentValidateFunc)
:SetDisabled(TSM.Operations.HasRelationship("Auctioning", private.currentOperationName, "bidPercent"))
:SetValue((operation.bidPercent * 100).."%")
:SetScript("OnValueChanged", private.BidPercentOnValueChanged)
)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY3")
:SetFormattedText(L["Enter a value from %d - %d%%"], 0, 100)
:SetTextColor(TSM.Operations.HasRelationship("Auctioning", private.currentOperationName, "bidPercent") and "TEXT_DISABLED" or "TEXT")
)
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedPriceInput("undercut", L["Undercut amount"], 66, private.CheckUndercut)
:SetMargin(0, 0, 0, 12)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedPriceInput("minPrice", L["Minimum price"], 126, BAD_PRICE_SOURCES))
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("priceReset", L["When below minimum:"])
:SetMargin(0, 0, 12, 12)
:AddChild(UIElements.New("SelectionDropdown", "dropdown")
:SetWidth(240)
:SetDisabled(TSM.Operations.HasRelationship("Auctioning", private.currentOperationName, "priceReset"))
:SetItems(BELOW_MIN_ITEMS, BELOW_MIN_KEYS)
:SetSettingInfo(operation, "priceReset")
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedPriceInput("maxPrice", L["Maximum price"], 126, BAD_PRICE_SOURCES))
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("aboveMax", L["When above maximum:"])
:SetMargin(0, 0, 12, 12)
:AddChild(UIElements.New("SelectionDropdown", "dropdown")
:SetWidth(240)
:SetDisabled(TSM.Operations.HasRelationship("Auctioning", private.currentOperationName, "aboveMax"))
:SetItems(ABOVE_MAX_ITEMS, ABOVE_MAX_KEYS)
:SetSettingInfo(operation, "aboveMax")
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedPriceInput("normalPrice", L["Normal price"], 126, BAD_PRICE_SOURCES))
)
end
function private.AddStackSizeSettings(frame)
if not TSM.IsWowClassic() then
return
end
frame:AddChild(private.CreateInputLine("stackSize", L["Stack size"], false, true))
frame:AddChild(private.CreateToggleLine("stackSizeIsCap", L["Allow partial stack"]))
end
function private.GetCancelingSettings()
TSM.UI.AnalyticsRecordPathChange("main", "operations", "auctioning", "canceling")
return UIElements.New("ScrollFrame", "settings")
:SetPadding(8, 8, 8, 0)
:AddChild(TSM.MainUI.Operations.CreateExpandableSection("Auctioning", "priceSettings", L["Canceling Options"], L["Adjust the settings below to set how groups attached to this operation will be cancelled."])
:AddChild(private.CreateToggleLine("cancelUndercut", L["Cancel undercut auctions"]))
:AddChild(private.CreateToggleLine("cancelRepost", L["Cancel to repost higher"]))
:AddChild(TSM.MainUI.Operations.CreateLinkedPriceInput("cancelRepostThreshold", L["Repost threshold"], 66))
)
end
function private.GetAuctioningSettings(self, button)
private.currentTab = button
if button == L["Details"] then
return private.GetDetailsSettings()
elseif button == L["Posting"] then
return private.GetPostingSettings()
elseif button == L["Canceling"] then
return private.GetCancelingSettings()
else
error("Unknown button!")
end
end
function private.AddBlacklistPlayers(frame)
local operation = TSM.Operations.GetSettings("Auctioning", private.currentOperationName)
if operation.blacklist == "" then return end
local containerFrame = UIElements.New("Frame", "blacklistFrame")
:SetLayout("FLOW")
for index, player in Vararg.Iterator(strsplit(",", operation.blacklist)) do
containerFrame:AddChild(UIElements.New("Frame", "blacklist" .. index)
:SetLayout("HORIZONTAL")
:SetSize(100, 20)
:SetMargin(0, 12, 0, 0)
:AddChild(UIElements.New("Text", "text")
:SetWidth("AUTO")
:SetMargin(0, 2, 0, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(player)
)
:AddChild(UIElements.New("Button", "removeBtn")
:SetBackgroundAndSize("iconPack.14x14/Close/Circle")
:SetContext(player)
:SetScript("OnClick", private.RemoveBlacklistOnClick)
)
:AddChild(UIElements.New("Spacer", "spacer"))
)
end
frame:AddChild(containerFrame)
end
function private.CreateInputLine(key, label, disabled, margin)
local operation = TSM.Operations.GetSettings("Auctioning", private.currentOperationName)
local hasRelationship = TSM.Operations.HasRelationship("Auctioning", private.currentOperationName, key)
return TSM.MainUI.Operations.CreateLinkedSettingLine(key, label, disabled)
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, margin and 12 or 4)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Input", "input")
:SetMargin(0, 8, 0, 0)
:SetBackgroundColor("ACTIVE_BG")
:SetContext(key)
:SetDisabled(hasRelationship or disabled)
:SetValidateFunc("CUSTOM_PRICE")
:SetSettingInfo(operation, key)
)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY3")
:SetTextColor((hasRelationship or disabled) and "TEXT_DISABLED" or "TEXT")
:SetFormattedText(L["Supported value range: %d - %d"], TSM.Operations.Auctioning.GetMinMaxValues(key))
)
)
end
function private.CreateToggleLine(key, label)
local operation = TSM.Operations.GetSettings("Auctioning", private.currentOperationName)
return TSM.MainUI.Operations.CreateLinkedSettingLine(key, label)
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("ToggleOnOff", "toggle")
:SetHeight(18)
:SetDisabled(TSM.Operations.HasRelationship("Auctioning", private.currentOperationName, key))
:SetSettingInfo(operation, key)
)
end
function private.CheckUndercut(_, value)
if not TSM.IsWowClassic() and Money.FromString(Money.ToString(value) or value) == 0 then
return true
elseif not TSM.IsWowClassic() and (Money.FromString(Money.ToString(value) or value) or math.huge) < COPPER_PER_SILVER then
return false, L["Invalid undercut. To post below the cheapest auction without a significant undercut, set your undercut to 0c."]
else
local isValid, err = CustomPrice.Validate(value)
if isValid then
return true
end
return false, L["Invalid custom price."].." "..err
end
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.IgnoreLowDurationOnSelectionChanged(self)
local operation = TSM.Operations.GetSettings("Auctioning", private.currentOperationName)
operation.ignoreLowDuration = self:GetSelectedItemKey() - 1
end
function private.BlacklistInputOnEnterPressed(input)
local newPlayer = input:GetValue()
if newPlayer == "" or strfind(newPlayer, ",") or newPlayer ~= String.Escape(newPlayer) then
-- this is an invalid player name
return
end
local operation = TSM.Operations.GetSettings("Auctioning", private.currentOperationName)
local found = false
for _, player in Vararg.Iterator(strsplit(",", operation.blacklist)) do
if newPlayer == player then
-- this player is already added
input:SetValue("")
found = true
end
end
if found then
return
end
operation.blacklist = (operation.blacklist == "") and newPlayer or (operation.blacklist..","..newPlayer)
input:GetElement("__parent.__parent.__parent.__parent.__parent.__parent"):ReloadContent()
end
function private.RemoveBlacklistOnClick(button)
local player = button:GetContext()
-- FIXME: This sort of logic should go within some Auctioning-specific operation setting wrapper code
local operation = TSM.Operations.GetSettings("Auctioning", private.currentOperationName)
if operation.blacklist == player then
operation.blacklist = ""
else
-- handle cases where this entry is at the start, in the middle, and at the end
operation.blacklist = gsub(operation.blacklist, "^"..player..",", "")
operation.blacklist = gsub(operation.blacklist, ","..player..",", ",")
operation.blacklist = gsub(operation.blacklist, ","..player.."$", "")
end
button:GetElement("__parent.__parent.__parent.__parent.__parent.__parent.__parent"):ReloadContent()
end
function private.SetAuctioningDuration(dropdown)
local operation = TSM.Operations.GetSettings("Auctioning", private.currentOperationName)
operation.duration = dropdown:GetSelectedItemKey()
end
function private.BidPercentValidateFunc(_, value)
value = strmatch(value, "^([0-9]+) *%%?$")
value = tonumber(value)
if not value or value < 0 or value > 100 then
return false, L["Bid percent must be between 0 and 100."]
end
return true
end
function private.BidPercentOnValueChanged(input)
local operation = TSM.Operations.GetSettings("Auctioning", private.currentOperationName)
local value = strmatch(input:GetValue(), "^([0-9]+) *%%?$")
value = tonumber(value) / 100
operation.bidPercent = value
end

View File

@@ -0,0 +1,867 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Operations = TSM.MainUI:NewPackage("Operations")
local L = TSM.Include("Locale").GetTable()
local Log = TSM.Include("Util.Log")
local Theme = TSM.Include("Util.Theme")
local Money = TSM.Include("Util.Money")
local TempTable = TSM.Include("Util.TempTable")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
local private = {
settings = nil,
moduleNames = {},
moduleCollapsed = {},
moduleCallbacks = {},
currentModule = nil,
currentOperationName = nil,
playerList = {},
linkMenuEntries = {},
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Operations.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "mainUIContext", "operationsDividedContainer")
:AddKey("global", "mainUIContext", "operationsSummaryScrollingTable")
TSM.MainUI.RegisterTopLevelPage(L["Operations"], private.GetOperationsFrame)
end
function Operations.RegisterModule(name, callback)
tinsert(private.moduleNames, name)
private.moduleCallbacks[name] = callback
end
function Operations.ShowOperationSettings(baseFrame, moduleName, operationName)
baseFrame:SetSelectedNavButton(L["Operations"], true)
baseFrame:GetElement("content.operations.selection.operationTree"):SetSelectedOperation(moduleName, operationName)
end
function Operations.GetOperationManagementElements(moduleName, operationName)
local operation = TSM.Operations.GetSettings(private.currentModule, private.currentOperationName)
wipe(private.playerList)
for factionrealm in TSM.db:GetConnectedRealmIterator("factionrealm") do
for _, character in TSM.db:FactionrealmCharacterIterator(factionrealm) do
tinsert(private.playerList, character.." - "..factionrealm)
end
end
return UIElements.New("Frame", "management")
:SetLayout("VERTICAL")
:AddChild(Operations.CreateExpandableSection(moduleName, "managementOptions", L["Management Options"], L["Below you can ignore this operation on certain characters or realms."])
:AddChild(Operations.CreateSettingLine("ignoreFactionRealms", L["Ignore operation on faction-realms"])
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("MultiselectionDropdown", "dropdown")
:SetHeight(24)
:SetItems(TSM.db:GetScopeKeys("factionrealm"), TSM.db:GetScopeKeys("factionrealm"))
:SetSelectionText(L["No Faction-Realms"], L["%d Faction-Realms"], L["All Faction-Realms"])
:SetSettingInfo(operation, "ignoreFactionrealm")
)
)
:AddChild(Operations.CreateSettingLine("ignoreCharacters", L["Ignore operation on characters"])
:SetLayout("VERTICAL")
:SetHeight(48)
:AddChild(UIElements.New("MultiselectionDropdown", "dropdown")
:SetHeight(24)
:SetItems(private.playerList, private.playerList)
:SetSelectionText(L["No Characters"], L["%d Characters"], L["All Characters"])
:SetSettingInfo(operation, "ignorePlayer")
)
)
)
:AddChild(Operations.CreateExpandableSection(moduleName, "groupManagement", L["Group Management"], L["Here you can add/remove what groups this operation is attached to."])
:AddChild(Operations.CreateSettingLine("applyNewGroup", L["Apply operation to group"])
:SetLayout("VERTICAL")
:SetHeight(48)
:AddChild(UIElements.New("GroupSelector", "group")
:SetHintText(L["Add operation to groups"])
:SetScript("OnSelectionChanged", private.GroupSelectionChanged)
)
)
:AddChildrenWithFunction(private.AddOperationGroups)
)
end
function Operations.CreateExpandableSection(moduleName, id, text, description)
return UIElements.New("CollapsibleContainer", id)
:SetLayout("VERTICAL")
:SetMargin(0, 0, 0, 8)
:SetContextTable(private.moduleCollapsed, moduleName..text)
:SetHeadingText(text)
:AddChild(UIElements.New("Text", "description")
:SetHeight(20)
:SetMargin(0, 0, 0, 12)
:SetFont("BODY_BODY3")
:SetText(description)
)
end
function Operations.CreateLinkedSettingLine(settingKey, labelText, disabled, alternateName)
local relationshipSet = TSM.Operations.HasRelationship(private.currentModule, private.currentOperationName, settingKey)
return UIElements.New("Frame", alternateName or settingKey)
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(0, 0, 0, 4)
:AddChild(UIElements.New("Frame", "line")
:SetLayout("HORIZONTAL")
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY2_MEDIUM")
:SetTextColor((relationshipSet or disabled) and "TEXT_DISABLED" or "TEXT")
:SetText(labelText)
)
:AddChild(private.CreateLinkButton(disabled, settingKey))
:AddChild(UIElements.New("Spacer", "spacer"))
)
end
function Operations.CreateSettingLine(id, labelText, disabled)
return UIElements.New("Frame", id)
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(0, 0, 0, 4)
:AddChild(UIElements.New("Text", "label")
:SetFont("BODY_BODY2_MEDIUM")
:SetTextColor(disabled and "TEXT_DISABLED" or "TEXT")
:SetText(labelText)
)
end
function Operations.CreateLinkedPriceInput(settingKey, label, height, validate, defaultValue)
local isDisabled = TSM.Operations.HasRelationship(private.currentModule, private.currentOperationName, settingKey)
local operation = TSM.Operations.GetSettings(private.currentModule, private.currentOperationName)
local value = operation[settingKey]
if defaultValue ~= nil and (not value or value == "") then
isDisabled = true
value = defaultValue
end
local validateFunc, validateContext = nil, nil
if type(validate) == "table" then
validateFunc = "CUSTOM_PRICE"
validateContext = validate
elseif type(validate) == "function" then
validateFunc = validate
elseif validate == nil then
validateFunc = "CUSTOM_PRICE"
else
error("Invalid validate: "..tostring(validate))
end
return Operations.CreateLinkedSettingLine(settingKey, label)
:SetLayout("VERTICAL")
:SetHeight(height)
:AddChild(UIElements.New("MultiLineInput", "input")
:SetHeight(height - 24)
:SetDisabled(isDisabled)
:SetValidateFunc(validateFunc, validateContext)
:SetSettingInfo(operation, settingKey)
:SetValue(Money.ToString(value) or value)
)
end
-- ============================================================================
-- Operations UI
-- ============================================================================
function private.GetOperationsFrame()
TSM.UI.AnalyticsRecordPathChange("main", "operations")
local frame = UIElements.New("DividedContainer", "operations")
:SetSettingsContext(private.settings, "operationsDividedContainer")
:SetMinWidth(250, 250)
:SetLeftChild(UIElements.New("Frame", "selection")
:SetLayout("VERTICAL")
:SetBackgroundColor("PRIMARY_BG_ALT")
:AddChild(UIElements.New("Input", "search")
:SetHeight(24)
:SetMargin(8, 8, 8, 16)
:SetIconTexture("iconPack.18x18/Search")
:SetClearButtonEnabled(true)
:AllowItemInsert(true)
:SetHintText(L["Search Operations"])
:SetScript("OnValueChanged", private.OperationSearchOnValueChanged)
)
:AddChild(UIElements.New("OperationTree", "operationTree")
:SetScript("OnOperationAdded", private.OperationTreeOnOperationAdded)
:SetScript("OnOperationDeleted", private.OperationTreeOnOperationConfirmDelete)
:SetScript("OnOperationSelected", private.OperationTreeOnOperationSelected)
)
)
:SetRightChild(UIElements.New("ViewContainer", "content")
:SetNavCallback(private.GetOperationsContent)
:AddPath("none", true)
:AddPath("summary")
:AddPath("operation")
)
return frame
end
function private.GetOperationsContent(_, path)
if path == "none" then
return UIElements.New("Frame", "settings")
:SetLayout("VERTICAL")
:SetWidth("EXPAND")
:SetBackgroundColor("PRIMARY_BG")
:AddChild(UIElements.New("Frame", "title")
:SetLayout("HORIZONTAL")
:SetHeight(40)
:SetPadding(8)
:AddChild(UIElements.New("Texture", "icon")
:SetMargin(0, 8, 0, 0)
:SetTextureAndSize(TSM.UI.TexturePacks.GetColoredKey("iconPack.18x18/Operation", "TEXT"))
)
:AddChild(UIElements.New("Text", "text")
:SetFont("BODY_BODY1_BOLD")
:SetText(L["No Operation Selected"])
)
)
:AddChild(UIElements.New("Spacer", "spacer"))
elseif path == "summary" then
return UIElements.New("Frame", "settings")
:SetLayout("VERTICAL")
:SetWidth("EXPAND")
:SetBackgroundColor("PRIMARY_BG")
:AddChild(UIElements.New("Frame", "title")
:SetLayout("HORIZONTAL")
:SetHeight(40)
:SetPadding(8)
:AddChild(UIElements.New("Text", "text")
:SetWidth("AUTO")
:SetFont("BODY_BODY1_BOLD")
)
:AddChild(UIElements.New("Spacer"))
:AddChild(UIElements.New("Button", "addBtn")
:SetWidth("AUTO")
:SetMargin(12, 12, 0, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetIcon("iconPack.14x14/Add/Circle", "LEFT")
:SetText(L["Create New"])
:SetScript("OnClick", private.CreateNewOperationOnClick)
)
)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("VERTICAL")
-- will be filled in by the operation selection callback
)
elseif path == "operation" then
return UIElements.New("Frame", "settings")
:SetLayout("VERTICAL")
:SetWidth("EXPAND")
:SetBackgroundColor("PRIMARY_BG")
:AddChild(UIElements.New("Frame", "title")
:SetLayout("HORIZONTAL")
:SetHeight(40)
:SetPadding(8)
:AddChild(UIElements.New("Texture", "icon")
:SetMargin(0, 8, 0, 0)
:SetTextureAndSize(TSM.UI.TexturePacks.GetColoredKey("iconPack.18x18/Operation", "TEXT"))
)
:AddChild(UIElements.New("EditableText", "text")
:SetWidth("AUTO")
:AllowItemInsert(true)
:SetFont("BODY_BODY1_BOLD")
:SetText(L["No Operation Selected"])
:SetScript("OnValueChanged", private.OperationNameChanged)
:SetScript("OnEditingChanged", private.NameOnEditingChanged)
)
:AddChild(UIElements.New("Spacer"))
:AddChild(UIElements.New("Button", "renameBtn")
:SetWidth("AUTO")
:SetMargin(12, 12, 0, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetIcon("iconPack.14x14/Edit", "LEFT")
:SetText(L["Rename"])
:SetScript("OnClick", private.RenameOperationOnClick)
)
:AddChild(UIElements.New("Button", "resetBtn")
:SetWidth("AUTO")
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetIcon("iconPack.14x14/Reset", "LEFT")
:SetText(L["Reset"])
:SetScript("OnClick", private.ResetOperationOnClick)
)
)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("VERTICAL")
-- will be filled in by the operation selection callback
)
else
error("Invalid path: "..tostring(path))
end
end
function private.GetSummaryContent()
local query = TSM.Operations.CreateQuery()
:Equal("moduleName", private.currentModule)
:VirtualField("numGroups", "number", private.NumGroupsVirtualField)
:VirtualField("numItems", "number", private.NumItemsVirtualField)
:OrderBy("operationName", true)
local mostGroupsName, mostGroupsValue = "---", -math.huge
local leastGroupsName, leastGroupsValue = "---", math.huge
local mostItemsName, mostItemsValue = "---", -math.huge
local leastItemsName, leastItemsValue = "---", math.huge
for _, row in query:Iterator() do
local operationName, numGroups, numItems = row:GetFields("operationName", "numGroups", "numItems")
if numGroups > mostGroupsValue then
mostGroupsValue = numGroups
mostGroupsName = operationName
end
if numGroups < leastGroupsValue then
leastGroupsValue = numGroups
leastGroupsName = operationName
end
if numItems > mostItemsValue then
mostItemsValue = numItems
mostItemsName = operationName
end
if numItems < leastItemsValue then
leastItemsValue = numItems
leastItemsName = operationName
end
end
return UIElements.New("Frame", "summary")
:SetLayout("VERTICAL")
:SetBackgroundColor("PRIMARY_BG")
:AddChild(UIElements.New("Frame", "summary")
:SetLayout("HORIZONTAL")
:SetHeight(48)
:SetMargin(8, 8, 0, 16)
:SetBackgroundColor("PRIMARY_BG_ALT", true)
:AddChild(UIElements.New("Frame", "groups")
:SetLayout("VERTICAL")
:SetPadding(8, 8, 2, 2)
:AddChild(UIElements.New("Frame", "most")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 0, 4)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetText(L["MOST GROUPS"])
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "value")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetText(mostGroupsName)
)
)
:AddChild(UIElements.New("Frame", "least")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetText(L["LEAST GROUPS"])
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "value")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetText(leastGroupsName)
)
)
)
:AddChild(UIElements.New("Texture", "line1")
:SetWidth(1)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Frame", "items")
:SetLayout("VERTICAL")
:SetPadding(8, 8, 2, 2)
:AddChild(UIElements.New("Frame", "most")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 0, 4)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetText(L["MOST ITEMS"])
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "value")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetText(mostItemsName)
)
)
:AddChild(UIElements.New("Frame", "least")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetTextColor("ACTIVE_BG_ALT")
:SetText(L["LEAST ITEMS"])
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Text", "value")
:SetWidth("AUTO")
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetText(leastItemsName)
)
)
)
)
:AddChild(UIElements.New("SelectionScrollingTable", "list")
:SetSettingsContext(private.settings, "operationsSummaryScrollingTable")
:GetScrollingTableInfo()
:NewColumn("name")
:SetTitle(L["Operation"])
:SetFont("TABLE_TABLE1")
:SetJustifyH("LEFT")
:SetTextInfo("operationName")
:SetSortInfo("operationName")
:SetActionIconInfo(1, 12, private.GetConfigureIcon, true)
:SetActionIconClickHandler(private.OnConfigureIconClick)
:DisableHiding()
:Commit()
:NewColumn("groups")
:SetTitle(L["Groups Using"])
:SetFont("TABLE_TABLE1")
:SetJustifyH("LEFT")
:SetTextInfo("numGroups")
:SetSortInfo("numGroups")
:Commit()
:NewColumn("items")
:SetTitle(L["Items Using"])
:SetFont("TABLE_TABLE1")
:SetJustifyH("LEFT")
:SetTextInfo("numItems", private.GetNumItemsText)
:SetSortInfo("numItems")
:SetTooltipInfo("numItems", private.GetNumItemsTooltip)
:Commit()
:Commit()
:SetQuery(query)
:SetContext(query)
:SetAutoReleaseQuery(true)
:SetScript("OnSelectionChanged", private.OperationListOnSelectionChanged)
)
:AddChild(UIElements.New("Texture", "line")
:SetHeight(2)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Frame", "footer")
:SetLayout("HORIZONTAL")
:SetHeight(40)
:SetPadding(8)
:SetBackgroundColor("PRIMARY_BG_ALT")
:AddChild(UIElements.New("ActionButton", "deleteSelected")
:SetHeight(24)
:SetMargin(0, 8, 0, 0)
:SetDisabled(true)
:SetText(L["Delete Operations"])
:SetScript("OnClick", private.DeleteSelectedOnClick)
)
:AddChild(UIElements.New("Button", "selectAll")
:SetSize("AUTO", 20)
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["Select All"])
:SetScript("OnClick", private.SelectAllOnClick)
)
:AddChild(UIElements.New("Texture", "line")
:SetSize(2, 20)
:SetMargin(0, 8, 0, 0)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Button", "clearAll")
:SetSize("AUTO", 20)
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["Clear All"])
:SetDisabled(true)
:SetScript("OnClick", private.ClearAllOnClick)
)
)
end
function private.AddOperationGroups(frame)
for _, groupPath in TSM.Operations.GroupIterator(private.currentModule, private.currentOperationName, true) do
frame:AddChild(private.CreateGroupOperationLine(groupPath))
end
end
function private.CreateGroupOperationLine(groupPath)
local groupName = groupPath == TSM.CONST.ROOT_GROUP_PATH and L["Base Group"] or TSM.Groups.Path.GetName(groupPath)
local level = select('#', strsplit(TSM.CONST.GROUP_SEP, groupPath))
return UIElements.New("Frame", "group")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(2, 0, 0, 0)
:AddChild(UIElements.New("Text", "text")
:SetWidth("AUTO")
:SetFont("BODY_BODY2")
:SetTextColor(Theme.GetGroupColor(level))
:SetText(groupName)
)
:AddChild(UIElements.New("Button", "viewBtn")
:SetMargin(2, 2, 0, 0)
:SetBackgroundAndSize("iconPack.14x14/Groups")
:SetContext(groupPath)
:SetScript("OnClick", private.ViewGroupOnClick)
)
:AddChild(UIElements.New("Button", "removeBtn")
:SetBackgroundAndSize("iconPack.14x14/Close/Default")
:SetContext(groupPath)
:SetScript("OnClick", private.RemoveOperationGroupOnClick)
)
:AddChild(UIElements.New("Spacer", "spacer"))
end
function private.CreateLinkButton(disabled, settingKey)
local relationshipSet = TSM.Operations.HasRelationship(private.currentModule, private.currentOperationName, settingKey)
local linkTexture = nil
if disabled and relationshipSet then
linkTexture = TSM.UI.TexturePacks.GetColoredKey("iconPack.14x14/Link", "INDICATOR_DISABLED")
elseif disabled then
linkTexture = TSM.UI.TexturePacks.GetColoredKey("iconPack.14x14/Link", "TEXT_DISABLED")
elseif relationshipSet then
linkTexture = TSM.UI.TexturePacks.GetColoredKey("iconPack.14x14/Link", "INDICATOR")
else
linkTexture = TSM.UI.TexturePacks.GetColoredKey("iconPack.14x14/Link", "TEXT")
end
return UIElements.New("Button", "linkBtn")
:SetMargin(4, 4, 0, 0)
:SetBackgroundAndSize(linkTexture)
:SetDisabled(disabled)
:SetContext(settingKey)
:SetScript("OnClick", private.LinkBtnOnClick)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OperationSearchOnValueChanged(input)
local filter = strlower(input:GetValue())
input:GetElement("__parent.operationTree"):SetOperationNameFilter(filter)
end
function private.OperationTreeOnOperationAdded(operationTree, moduleName, operationName, copyOperationName)
-- clear the filter
operationTree:GetElement("__parent.search")
:SetValue("")
:Draw()
operationTree:SetOperationNameFilter("")
TSM.Operations.Create(moduleName, operationName)
if copyOperationName then
TSM.Operations.Copy(moduleName, operationName, copyOperationName)
end
end
function private.OperationTreeOnOperationConfirmDelete(self, moduleName, operationName)
self:GetBaseElement():ShowConfirmationDialog(L["Delete Operation?"], L["Are you sure you want to delete this operation?"], private.OperationTreeOnOperationDeleted, self, moduleName, operationName)
end
function private.OperationTreeOnOperationDeleted(self, moduleName, operationName)
TSM.Operations.Delete(moduleName, operationName)
local operationTree = self:GetElement("__parent.operationTree")
operationTree:SetSelectedOperation(moduleName, nil)
:Draw()
end
function private.OperationTreeOnOperationSelected(self, moduleName, operationName)
private.currentModule = moduleName
private.currentOperationName = operationName
local viewContainer = self:GetParentElement():GetParentElement():GetElement("content")
if moduleName and operationName then
TSM.Operations.Update(moduleName, operationName)
viewContainer:SetPath("operation")
viewContainer:GetElement("settings.title.text"):SetText(operationName)
local contentFrame = viewContainer:GetElement("settings.content")
contentFrame:ReleaseAllChildren()
contentFrame:AddChild(private.moduleCallbacks[moduleName](operationName))
elseif moduleName then
local numOperations = 0
for _ in TSM.Operations.OperationIterator(moduleName) do
numOperations = numOperations + 1
end
TSM.UI.AnalyticsRecordPathChange("main", "operations", "summary")
viewContainer:SetPath("summary")
viewContainer:GetElement("settings.title.text"):SetText(format(L["%s %s Operations"], Theme.GetColor("INDICATOR"):ColorText(numOperations), moduleName))
local contentFrame = viewContainer:GetElement("settings.content")
contentFrame:ReleaseAllChildren()
contentFrame:AddChild(private.GetSummaryContent())
else
TSM.UI.AnalyticsRecordPathChange("main", "operations", "none")
viewContainer:SetPath("none")
viewContainer:GetElement("settings.title.text"):SetText(L["No Operation Selected"])
end
viewContainer:Draw()
end
function private.CreateNewOperationOnClick(button)
local operationName = "New Operation"
local num = 1
while TSM.Operations.Exists(private.currentModule, operationName.." "..num) do
num = num + 1
end
operationName = operationName .. " " .. num
TSM.Operations.Create(private.currentModule, operationName)
button:GetElement("__parent.__parent.__parent.__parent.selection.operationTree")
:SetSelectedOperation(private.currentModule, operationName)
:Draw()
end
function private.OperationNameChanged(text, newValue)
newValue = strtrim(newValue)
if newValue == private.currentOperationName then
-- didn't change
text:Draw()
elseif newValue == "" then
Log.PrintUser(L["Invalid operation name."])
text:Draw()
elseif TSM.Operations.Exists(private.currentModule, newValue) then
Log.PrintUser(L["Group already exists."])
text:Draw()
else
TSM.Operations.Rename(private.currentModule, private.currentOperationName, newValue)
text:GetElement("__parent.__parent.__parent.__parent.selection.operationTree")
:SetSelectedOperation(private.currentModule, newValue)
:Draw()
end
end
function private.NameOnEditingChanged(text, editing)
if editing then
text:GetElement("__parent.renameBtn"):Hide()
else
text:GetElement("__parent.renameBtn"):Show()
end
end
function private.RenameOperationOnClick(button)
button:GetElement("__parent.text"):SetEditing(true)
end
function private.ResetOperationOnClick(button)
button:GetBaseElement():ShowConfirmationDialog(L["Reset Operation?"], L["Resetting the operation will return all inputs back to default and cannot be unddone. Click confirm to reset."], private.ConfirmResetOnClick, button)
end
function private.ConfirmResetOnClick(button)
TSM.Operations.Reset(private.currentModule, private.currentOperationName)
local settingsFrame = button:GetBaseElement():GetElement("content.operations.content.settings")
local contentFrame = settingsFrame:GetElement("content")
contentFrame:ReleaseAllChildren()
TSM.Operations.Update(private.currentModule, private.currentOperationName)
contentFrame:AddChild(private.moduleCallbacks[private.currentModule](private.currentOperationName))
button:GetBaseElement():HideDialog()
settingsFrame:Draw()
Log.PrintfUser(L["%s - %s has been reset to default values."], private.currentModule, Theme.GetColor("INDICATOR_ALT"):ColorText(private.currentOperationName))
end
function private.GroupSelectionChanged(groupSelector)
for groupPath in groupSelector:SelectedGroupIterator() do
if not TSM.Operations.GroupHasOperation(private.currentModule, groupPath, private.currentOperationName) then
local parentElement = groupSelector:GetParentElement():GetParentElement()
if groupPath ~= TSM.CONST.ROOT_GROUP_PATH then
TSM.Groups.SetOperationOverride(groupPath, private.currentModule, true)
end
local numOperations = 0
local lastOperationName = nil
for _, groupOperationName in TSM.Groups.OperationIterator(groupPath, private.currentModule) do
lastOperationName = groupOperationName
numOperations = numOperations + 1
end
if numOperations == TSM.Operations.GetMaxNumber(private.currentModule) then
-- replace the last operation since we're already at the max number of operations
TSM.Groups.RemoveOperation(groupPath, private.currentModule, numOperations)
Log.PrintfUser(L["%s previously had the max number of operations, so removed %s."], Log.ColorUserAccentText(TSM.Groups.Path.Format(groupPath)), Log.ColorUserAccentText(lastOperationName))
end
TSM.Groups.AppendOperation(groupPath, private.currentModule, private.currentOperationName)
Log.PrintfUser(L["Added %s to %s."], Log.ColorUserAccentText(private.currentOperationName), Log.ColorUserAccentText(groupPath == TSM.CONST.ROOT_GROUP_PATH and L["Base Group"] or TSM.Groups.Path.Format(groupPath)))
parentElement:AddChild(private.CreateGroupOperationLine(groupPath))
end
end
groupSelector:ClearSelectedGroups(true)
groupSelector:GetParentElement():GetParentElement():GetParentElement():GetParentElement():GetParentElement():GetParentElement():Draw()
end
function private.ViewGroupOnClick(button)
local baseFrame = button:GetBaseElement()
TSM.MainUI.Groups.ShowGroupSettings(baseFrame, button:GetContext())
end
function private.RemoveOperationGroupOnClick(self)
local groupPath = self:GetContext()
TSM.Groups.RemoveOperationByName(groupPath, private.currentModule, private.currentOperationName)
-- remove the line for this group
local removeElement = self:GetParentElement()
local removeElementParent = removeElement:GetParentElement()
removeElementParent:RemoveChild(removeElement)
removeElement:Release()
removeElementParent:GetParentElement():GetParentElement():GetParentElement():Draw()
end
function private.LinkBtnOnClick(button)
local settingKey = button:GetContext()
wipe(private.linkMenuEntries)
for _, operationName in TSM.Operations.OperationIterator(private.currentModule) do
if operationName ~= private.currentOperationName and not TSM.Operations.IsCircularRelationship(private.currentModule, private.currentOperationName, settingKey) then
tinsert(private.linkMenuEntries, operationName)
end
end
sort(private.linkMenuEntries)
button:GetBaseElement():ShowDialogFrame(UIElements.New("PopupFrame", "linkDialog")
:SetLayout("VERTICAL")
:SetSize(263, 243)
:AddAnchor("TOPRIGHT", button:_GetBaseFrame(), "BOTTOM", 22, -16)
:AddChild(UIElements.New("Frame", "titleFrame")
:SetLayout("VERTICAL")
:SetHeight(37)
:AddChild(UIElements.New("Text", "title")
:SetFont("BODY_BODY2_MEDIUM")
:SetJustifyH("CENTER")
:SetText(L["Link to Another Operation"])
)
)
:AddChild(UIElements.New("Texture", "line")
:SetHeight(2)
:SetTexture("TEXT")
)
:AddChild(UIElements.New("SelectionList", "list")
:SetContext(settingKey)
:SetMargin(2, 2, 0, 3)
:SetEntries(private.linkMenuEntries, TSM.Operations.GetRelationship(private.currentModule, private.currentOperationName, settingKey))
:SetScript("OnEntrySelected", private.ListOnEntrySelected)
)
)
end
function private.ListOnEntrySelected(list, operationName)
local settingKey = list:GetContext()
local previousValue = TSM.Operations.GetRelationship(private.currentModule, private.currentOperationName, settingKey)
if operationName == previousValue then
TSM.Operations.SetRelationship(private.currentModule, private.currentOperationName, settingKey, nil)
else
TSM.Operations.SetRelationship(private.currentModule, private.currentOperationName, settingKey, operationName)
end
local baseFrame = list:GetBaseElement()
baseFrame:HideDialog()
Operations.ShowOperationSettings(baseFrame, private.currentModule, private.currentOperationName)
end
function private.OperationListOnSelectionChanged(scrollingTable)
local selectionCleared = scrollingTable:IsSelectionCleared()
local numSelected = 0
for _ in scrollingTable:SelectionIterator() do
numSelected = numSelected + 1
end
local footer = scrollingTable:GetElement("__parent.footer")
footer:GetElement("deleteSelected")
:SetText(numSelected > 0 and format(L["Delete %d Operations"], numSelected) or L["Delete Operations"])
:SetDisabled(selectionCleared)
footer:GetElement("selectAll")
:SetDisabled(scrollingTable:IsAllSelected())
footer:GetElement("clearAll")
:SetDisabled(selectionCleared)
footer:Draw()
end
function private.SelectAllOnClick(button)
button:GetElement("__parent.__parent.list"):SelectAll()
end
function private.ClearAllOnClick(button)
button:GetElement("__parent.__parent.list"):ClearSelection()
end
function private.DeleteSelectedOnClick(button)
local scrollingTable = button:GetElement("__parent.__parent.list")
button:GetBaseElement():ShowConfirmationDialog(L["Delete Operations?"], L["Are you sure you want to delete the selected operations?"], private.DeleteSelectedOperations, scrollingTable)
end
function private.DeleteSelectedOperations(scrollingTable)
local toDelete = TempTable.Acquire()
for _, row in scrollingTable:SelectionIterator() do
local moduleName, operationName = row:GetFields("moduleName", "operationName")
assert(moduleName == private.currentModule)
tinsert(toDelete, operationName)
end
TSM.Operations.DeleteList(private.currentModule, toDelete)
TempTable.Release(toDelete)
scrollingTable:UpdateData(true)
private.OperationListOnSelectionChanged(scrollingTable)
scrollingTable:GetElement("__parent.__parent.__parent.__parent.__parent.selection.operationTree"):UpdateData(true)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.NumGroupsVirtualField(row)
local num = 0
for _ in TSM.Operations.GroupIterator(row:GetField("moduleName"), row:GetField("operationName")) do
num = num + 1
end
return num
end
function private.NumItemsVirtualField(row)
local includesBaseGroup = false
local num = 0
for _, groupPath in TSM.Operations.GroupIterator(row:GetField("moduleName"), row:GetField("operationName")) do
if groupPath == TSM.CONST.ROOT_GROUP_PATH then
includesBaseGroup = true
else
num = num + TSM.Groups.GetNumItems(groupPath)
end
end
if includesBaseGroup then
num = num + 0.9
end
return num
end
function private.GetConfigureIcon(_, _, iconIndex)
assert(iconIndex == 1)
return true, "iconPack.12x12/Popout", false
end
function private.OnConfigureIconClick(scrollingTable, data, iconIndex)
assert(iconIndex == 1)
local operationName = scrollingTable:GetContext():GetResultRowByUUID(data):GetField("operationName")
scrollingTable:GetElement("__parent.__parent.__parent.__parent.__parent.selection.operationTree")
:SetSelectedOperation(private.currentModule, operationName)
end
function private.GetNumItemsText(numItems)
if numItems == floor(numItems) then
return numItems
else
return floor(numItems).."*"
end
end
function private.GetNumItemsTooltip(numItems)
if numItems == floor(numItems) then
return nil
end
return L["This operation is applied to the base group which includes every item not in another group."]
end

View File

@@ -0,0 +1,159 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Crafting = TSM.MainUI.Operations:NewPackage("Crafting")
local L = TSM.Include("Locale").GetTable()
local UIElements = TSM.Include("UI.UIElements")
local private = {
currentOperationName = nil,
}
local BAD_CRAFT_VALUE_PRICE_SOURCES = {
crafting = true,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Crafting.OnInitialize()
TSM.MainUI.Operations.RegisterModule("Crafting", private.GetCraftingOperationSettings)
end
-- ============================================================================
-- Crafting Operation Settings UI
-- ============================================================================
function private.GetCraftingOperationSettings(operationName)
TSM.UI.AnalyticsRecordPathChange("main", "operations", "crafting")
private.currentOperationName = operationName
local operation = TSM.Operations.GetSettings("Crafting", private.currentOperationName)
local frame = UIElements.New("ScrollFrame", "settings")
:SetPadding(8, 8, 8, 0)
:AddChild(TSM.MainUI.Operations.CreateExpandableSection("Crafting", "restockQuantity", L["Restock Options"], L["Adjust how crafted items are restocked."])
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("minRestock", L["Min restock quantity"])
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Input", "input")
:SetMargin(0, 8, 0, 0)
:SetBackgroundColor("ACTIVE_BG")
:SetValidateFunc("CUSTOM_PRICE")
:SetSettingInfo(operation, "minRestock")
:SetDisabled(TSM.Operations.HasRelationship("Crafting", private.currentOperationName, "minRestock"))
)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY3")
:SetTextColor(TSM.Operations.HasRelationship("Crafting", private.currentOperationName, "minRestock") and "TEXT_DISABLED" or "TEXT")
:SetFormattedText(L["Supported range: %d - %d"], TSM.Operations.Crafting.GetRestockRange())
)
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("maxRestock", L["Max restock quantity"])
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Input", "input")
:SetMargin(0, 8, 0, 0)
:SetBackgroundColor("ACTIVE_BG")
:SetValidateFunc("CUSTOM_PRICE")
:SetSettingInfo(operation, "maxRestock")
:SetDisabled(TSM.Operations.HasRelationship("Crafting", private.currentOperationName, "maxRestock"))
)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY3")
:SetTextColor(TSM.Operations.HasRelationship("Crafting", private.currentOperationName, "maxRestock") and "TEXT_DISABLED" or "TEXT")
:SetFormattedText(L["Supported range: %d - %d"], TSM.Operations.Crafting.GetRestockRange())
)
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("minProfit", L["Set min profit"], nil, "minProfitToggle")
:SetLayout("VERTICAL")
:SetHeight(42)
:AddChild(UIElements.New("ToggleOnOff", "toggle")
:SetHeight(18)
:SetValue(operation.minProfit ~= "")
:SetDisabled(TSM.Operations.HasRelationship("Crafting", private.currentOperationName, "minProfit"))
:SetScript("OnValueChanged", private.MinProfitToggleOnValueChanged)
)
)
)
:AddChild(TSM.MainUI.Operations.CreateExpandableSection("Crafting", "priceSettings", L["Crafting Value"], L["Adjust how TSM values crafted items when calculating profit."])
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("craftPriceMethod", L["Override default craft value"], nil, "craftPriceMethodToggle")
:SetLayout("VERTICAL")
:SetHeight(42)
:AddChild(UIElements.New("ToggleOnOff", "toggle")
:SetHeight(18)
:SetValue(operation.craftPriceMethod ~= "")
:SetDisabled(TSM.Operations.HasRelationship("Crafting", private.currentOperationName, "craftPriceMethod"))
:SetScript("OnValueChanged", private.CraftPriceToggleOnValueChanged)
)
)
)
:AddChild(TSM.MainUI.Operations.GetOperationManagementElements("Crafting", private.currentOperationName))
if operation.minProfit ~= "" then
frame:GetElement("restockQuantity.content.minProfitToggle"):SetMargin(0, 0, 0, 12)
frame:GetElement("restockQuantity"):AddChild(TSM.MainUI.Operations.CreateLinkedPriceInput("minProfit", L["Min profit amount"], 80))
end
if operation.craftPriceMethod ~= "" then
frame:GetElement("priceSettings.content.craftPriceMethodToggle"):SetMargin(0, 0, 0, 12)
frame:GetElement("priceSettings"):AddChild(TSM.MainUI.Operations.CreateLinkedPriceInput("craftPriceMethod", L["Craft Value"], 80, BAD_CRAFT_VALUE_PRICE_SOURCES, TSM.db.global.craftingOptions.defaultCraftPriceMethod))
end
return frame
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.MinProfitToggleOnValueChanged(toggle, value)
local operation = TSM.Operations.GetSettings("Crafting", private.currentOperationName)
local defaultValue = TSM.Operations.GetSettingDefault("Crafting", "minProfit")
operation.minProfit = value and defaultValue or ""
local settingsFrame = toggle:GetParentElement():GetParentElement()
if value then
settingsFrame:GetElement("minProfitToggle"):SetMargin(0, 0, 0, 12)
settingsFrame:GetParentElement():AddChild(TSM.MainUI.Operations.CreateLinkedPriceInput("minProfit", L["Min profit amount"], 80))
else
settingsFrame:GetElement("minProfitToggle"):SetMargin(0, 0, 0, 0)
local linkedPriceLine = settingsFrame:GetElement("minProfit")
settingsFrame:RemoveChild(linkedPriceLine)
linkedPriceLine:Release()
end
settingsFrame:GetParentElement():GetParentElement():Draw()
end
function private.CraftPriceToggleOnValueChanged(toggle, value)
local operation = TSM.Operations.GetSettings("Crafting", private.currentOperationName)
operation.craftPriceMethod = value and TSM.db.global.craftingOptions.defaultCraftPriceMethod or ""
local settingsFrame = toggle:GetParentElement():GetParentElement()
if value then
settingsFrame:GetElement("craftPriceMethodToggle"):SetMargin(0, 0, 0, 12)
settingsFrame:GetParentElement():AddChild(TSM.MainUI.Operations.CreateLinkedPriceInput("craftPriceMethod", L["Craft Value"], 80, BAD_CRAFT_VALUE_PRICE_SOURCES, TSM.db.global.craftingOptions.defaultCraftPriceMethod))
else
settingsFrame:GetElement("craftPriceMethodToggle"):SetMargin(0, 0, 0, 0)
local linkedPriceLine = settingsFrame:GetElement("craftPriceMethod")
settingsFrame:RemoveChild(linkedPriceLine)
linkedPriceLine:Release()
end
settingsFrame:GetParentElement():GetParentElement():Draw()
end

View File

@@ -0,0 +1,220 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Mailing = TSM.MainUI.Operations:NewPackage("Mailing")
local PlayerInfo = TSM.Include("Service.PlayerInfo")
local L = TSM.Include("Locale").GetTable()
local UIElements = TSM.Include("UI.UIElements")
local private = {
currentOperationName = nil,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Mailing.OnInitialize()
TSM.MainUI.Operations.RegisterModule("Mailing", private.GetMailingOperationSettings)
end
-- ============================================================================
-- Mailing Operation Settings UI
-- ============================================================================
function private.GetMailingOperationSettings(operationName)
TSM.UI.AnalyticsRecordPathChange("main", "operations", "mailing")
private.currentOperationName = operationName
local operation = TSM.Operations.GetSettings("Mailing", private.currentOperationName)
return UIElements.New("ScrollFrame", "content")
:SetPadding(8, 8, 8, 0)
:AddChild(TSM.MainUI.Operations.CreateExpandableSection("Mailing", "generalOptions", L["General Options"], L["Adjust how items are mailed."])
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("target", L["Target character"])
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("HORIZONTAL")
:AddChild(UIElements.New("Input", "input")
:SetHeight(24)
:SetMargin(0, 8, 0, 0)
:SetHintText(L["Enter player name"])
:SetAutoComplete(PlayerInfo.GetConnectedAlts())
:SetClearButtonEnabled(true)
:SetDisabled(TSM.Operations.HasRelationship("Mailing", private.currentOperationName, "target"))
:SetSettingInfo(operation, "target")
)
:AddChild(UIElements.New("ActionButton", "contacts")
:SetSize(152, 24)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Contacts"])
:SetScript("OnClick", private.ContactsBtnOnClick)
)
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("keepQty", L["Keep this amount"])
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Input", "input")
:SetMargin(0, 8, 0, 0)
:SetBackgroundColor("ACTIVE_BG")
:SetValidateFunc("NUMBER", "0:50000")
:SetSettingInfo(operation, "keepQty")
:SetDisabled(TSM.Operations.HasRelationship("Mailing", private.currentOperationName, "keepQty"))
)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY3")
:SetTextColor(TSM.Operations.HasRelationship("Mailing", private.currentOperationName, "keepQty") and "TEXT_DISABLED" or "TEXT")
:SetFormattedText(L["Enter a value from %d - %d"], 0, 50000)
)
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("maxQtyEnabled", L["Set max quantity"])
:SetLayout("VERTICAL")
:SetHeight(42)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("ToggleOnOff", "toggle")
:SetHeight(18)
:SetSettingInfo(operation, "maxQtyEnabled")
:SetDisabled(TSM.Operations.HasRelationship("Mailing", private.currentOperationName, "maxQtyEnabled"))
:SetScript("OnValueChanged", private.MaxQuantityToggleOnValueChanged)
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("maxQty", L["Max quantity"], not operation.maxQtyEnabled)
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Input", "input")
:SetMargin(0, 8, 0, 0)
:SetBackgroundColor("ACTIVE_BG")
:SetValidateFunc("NUMBER", "1:50000")
:SetDisabled(TSM.Operations.HasRelationship("Mailing", private.currentOperationName, "maxQty") or not operation.maxQtyEnabled)
:SetSettingInfo(operation, "maxQty")
)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY3")
:SetTextColor((TSM.Operations.HasRelationship("Mailing", private.currentOperationName, "maxQty") or not operation.maxQtyEnabled) and "TEXT_DISABLED" or "TEXT")
:SetFormattedText(L["Enter a value from %d - %d"], 1, 50000)
)
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("restock", L["Restock target to max quantity"], not operation.maxQtyEnabled)
:SetLayout("VERTICAL")
:SetHeight(42)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("ToggleOnOff", "toggle")
:SetHeight(18)
:SetSettingInfo(operation, "restock")
:SetDisabled(TSM.Operations.HasRelationship("Mailing", private.currentOperationName, "restock") or not operation.maxQtyEnabled)
:SetScript("OnValueChanged", private.RestockToggleOnValueChanged)
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("restockSources", L["Sources to include for restock"], not operation.restock or not operation.maxQtyEnabled)
:SetLayout("VERTICAL")
:SetHeight(48)
:AddChild(UIElements.New("MultiselectionDropdown", "dropdown")
:SetHeight(24)
:AddItem(BANK, "bank")
:AddItem(GUILD, "guild")
:SetSettingInfo(operation, "restockSources")
:SetSelectionText(L["No Sources"], L["%d Sources"], L["All Sources"])
:SetDisabled(TSM.Operations.HasRelationship("Mailing", private.currentOperationName, "restockSources") or not operation.restock or not operation.maxQtyEnabled)
)
)
)
:AddChild(TSM.MainUI.Operations.GetOperationManagementElements("Mailing", private.currentOperationName))
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.ContactsBtnOnClick(button)
TSM.UI.Util.Contacts.ShowDialog(button, button:GetElement("__parent.input"))
end
function private.MaxQuantityToggleOnValueChanged(toggle, value)
local settingsFrame = toggle:GetElement("__parent.__parent")
local restockValue = settingsFrame:GetElement("restock.toggle"):GetValue()
local relationshipSet, linkTexture, textColor = TSM.Operations.GetRelationshipColors("Mailing", private.currentOperationName, "maxQty", value)
settingsFrame:GetElement("maxQty.line.linkBtn")
:SetBackground(linkTexture)
:SetDisabled(not value)
settingsFrame:GetElement("maxQty.line.label")
:SetTextColor(textColor)
settingsFrame:GetElement("maxQty.content.label")
:SetTextColor(textColor)
settingsFrame:GetElement("maxQty.content.input")
:SetDisabled(relationshipSet or not value)
relationshipSet, linkTexture, textColor = TSM.Operations.GetRelationshipColors("Mailing", private.currentOperationName, "restock", value)
settingsFrame:GetElement("restock.line.linkBtn")
:SetBackground(linkTexture)
:SetDisabled(not value)
settingsFrame:GetElement("restock.line.label")
:SetTextColor(textColor)
settingsFrame:GetElement("restock.toggle")
:SetDisabled(relationshipSet or not value)
relationshipSet = TSM.Operations.HasRelationship("Mailing", private.currentOperationName, "restockSources")
if relationshipSet and value and restockValue then
linkTexture = TSM.UI.TexturePacks.GetColoredKey("iconPack.14x14/Link", "INDICATOR")
elseif (relationshipSet and not value and restockValue) or (relationshipSet and value and not restockValue) or (relationshipSet and not value and not restockValue) then
linkTexture = TSM.UI.TexturePacks.GetColoredKey("iconPack.14x14/Link", "INDICATOR_DISABLED")
elseif (value and not restockValue) or (not value and restockValue) or (not value and not restockValue) then
linkTexture = TSM.UI.TexturePacks.GetColoredKey("iconPack.14x14/Link", "TEXT_DISABLED")
else
linkTexture = TSM.UI.TexturePacks.GetColoredKey("iconPack.14x14/Link", "TEXT")
end
settingsFrame:GetElement("restockSources.line.linkBtn")
:SetBackground(linkTexture)
:SetDisabled(not value)
settingsFrame:GetElement("restockSources.line.label")
:SetTextColor(relationshipSet and "TEXT_DISABLED" or ((value and restockValue) and "TEXT" or "TEXT_DISABLED"))
settingsFrame:GetElement("restockSources.dropdown")
:SetDisabled(relationshipSet or not value or not restockValue)
settingsFrame:Draw()
end
function private.RestockToggleOnValueChanged(toggle, value)
local settingsFrame = toggle:GetElement("__parent.__parent")
local maxQtyEnabled = settingsFrame:GetElement("maxQtyEnabled.toggle"):GetValue()
local relationshipSet = TSM.Operations.HasRelationship("Mailing", private.currentOperationName, "restockSources")
local linkTexture = nil
if relationshipSet and value and maxQtyEnabled then
linkTexture = TSM.UI.TexturePacks.GetColoredKey("iconPack.14x14/Link", "INDICATOR")
elseif (relationshipSet and not value and maxQtyEnabled) or (relationshipSet and value and not maxQtyEnabled) or (relationshipSet and not value and not maxQtyEnabled) then
linkTexture = TSM.UI.TexturePacks.GetColoredKey("iconPack.14x14/Link", "INDICATOR_DISABLED")
elseif (value and not maxQtyEnabled) or (not value and maxQtyEnabled) or (not value and not maxQtyEnabled) then
linkTexture = TSM.UI.TexturePacks.GetColoredKey("iconPack.14x14/Link", "TEXT_DISABLED")
else
linkTexture = TSM.UI.TexturePacks.GetColoredKey("iconPack.14x14/Link", "TEXT")
end
settingsFrame:GetElement("restockSources.line.linkBtn")
:SetBackground(linkTexture)
:SetDisabled(not value)
settingsFrame:GetElement("restockSources.line.label")
:SetTextColor(relationshipSet and "TEXT_DISABLED" or ((value and maxQtyEnabled) and "TEXT" or "TEXT_DISABLED"))
settingsFrame:GetElement("restockSources.dropdown")
:SetDisabled(relationshipSet or not value or not maxQtyEnabled)
settingsFrame:Draw()
end

View File

@@ -0,0 +1,88 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Shopping = TSM.MainUI.Operations:NewPackage("Shopping")
local L = TSM.Include("Locale").GetTable()
local UIElements = TSM.Include("UI.UIElements")
local private = {
currentOperationName = nil,
}
local RESTOCK_SOURCES = { L["Alts"], L["Auctions"], BANK, GUILD }
local RESTOCK_SOURCES_KEYS = { "alts", "auctions", "bank", "guild" }
local BAD_PRICE_SOURCES = { shoppingopmax = true }
-- ============================================================================
-- Module Functions
-- ============================================================================
function Shopping.OnInitialize()
TSM.MainUI.Operations.RegisterModule("Shopping", private.GetShoppingOperationSettings)
end
-- ============================================================================
-- Shopping Operation Settings UI
-- ============================================================================
function private.GetShoppingOperationSettings(operationName)
TSM.UI.AnalyticsRecordPathChange("main", "operations", "shopping")
private.currentOperationName = operationName
local operation = TSM.Operations.GetSettings("Shopping", private.currentOperationName)
return UIElements.New("ScrollFrame", "settings")
:SetPadding(8, 8, 8, 0)
:SetBackgroundColor("PRIMARY_BG")
:AddChild(TSM.MainUI.Operations.CreateExpandableSection("Shopping", "generalOptions", L["General Options"], L["Set what items are shown during a Shopping scan."])
:AddChild(TSM.MainUI.Operations.CreateLinkedPriceInput("maxPrice", L["Maximum auction price"], 124, BAD_PRICE_SOURCES))
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("showAboveMaxPrice", L["Show auctions above max price"])
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 12, 12)
:AddChild(UIElements.New("ToggleOnOff", "toggle")
:SetHeight(18)
:SetSettingInfo(operation, "showAboveMaxPrice")
:SetDisabled(TSM.Operations.HasRelationship("Shopping", private.currentOperationName, "showAboveMaxPrice"))
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("restockQuantity", L["Maximum restock quantity"])
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Input", "input")
:SetMargin(0, 8, 0, 0)
:SetBackgroundColor("ACTIVE_BG")
:SetValidateFunc("CUSTOM_PRICE")
:SetSettingInfo(operation, "restockQuantity")
:SetDisabled(TSM.Operations.HasRelationship("Shopping", private.currentOperationName, "restockQuantity"))
)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY3")
:SetTextColor(TSM.Operations.HasRelationship("Shopping", private.currentOperationName, "restockQuantity") and "TEXT_DISABLED" or "TEXT")
:SetFormattedText(L["Supported range: %d - %d"], TSM.Operations.Shopping.GetRestockRange())
)
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("restockSources", L["Sources to include for restock"])
:SetLayout("VERTICAL")
:SetHeight(48)
:AddChild(UIElements.New("MultiselectionDropdown", "dropdown")
:SetHeight(24)
:SetItems(RESTOCK_SOURCES, RESTOCK_SOURCES_KEYS)
:SetSettingInfo(operation, "restockSources")
:SetSelectionText(L["No Sources"], L["%d Sources"], L["All Sources"])
:SetDisabled(TSM.Operations.HasRelationship("Shopping", private.currentOperationName, "restockSources"))
)
)
)
:AddChild(TSM.MainUI.Operations.GetOperationManagementElements("Shopping", private.currentOperationName))
end

View File

@@ -0,0 +1,42 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Sniper = TSM.MainUI.Operations:NewPackage("Sniper")
local L = TSM.Include("Locale").GetTable()
local UIElements = TSM.Include("UI.UIElements")
local private = {
currentOperationName = nil,
}
local BAD_PRICE_SOURCES = { sniperopmax = true }
-- ============================================================================
-- Module Functions
-- ============================================================================
function Sniper.OnInitialize()
TSM.MainUI.Operations.RegisterModule("Sniper", private.GetSniperOperationSettings)
end
-- ============================================================================
-- Sniper Operation Settings UI
-- ============================================================================
function private.GetSniperOperationSettings(operationName)
TSM.UI.AnalyticsRecordPathChange("main", "operations", "sniper")
private.currentOperationName = operationName
return UIElements.New("ScrollFrame", "settings")
:SetPadding(8, 8, 8, 0)
:SetBackgroundColor("PRIMARY_BG")
:AddChild(TSM.MainUI.Operations.CreateExpandableSection("Sniper", "settings", L["General Options"], L["Set what items are shown during a Sniper scan."])
:AddChild(TSM.MainUI.Operations.CreateLinkedPriceInput("belowPrice", L["Maximum price"], 124, BAD_PRICE_SOURCES))
)
:AddChild(TSM.MainUI.Operations.GetOperationManagementElements("Sniper", private.currentOperationName))
end

View File

@@ -0,0 +1,268 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Vendoring = TSM.MainUI.Operations:NewPackage("Vendoring")
local L = TSM.Include("Locale").GetTable()
local UIElements = TSM.Include("UI.UIElements")
local private = {
currentOperationName = nil,
}
local RESTOCK_SOURCES = { BANK, GUILD, L["Alts"], L["Alts AH"], L["AH"], L["Mail"] }
local RESTOCK_SOURCES_KEYS = { "bank", "guild", "alts", "alts_ah", "ah", "mail" }
local SETTING_INFO = {
restockQty = "INPUT_LABEL",
restockSources = "DROPDOWN",
keepQty = "INPUT_LABEL",
sellAfterExpired = "INPUT_LABEL",
vsMarketValue = "INPUT",
vsMaxMarketValue = "INPUT",
vsDestroyValue = "INPUT",
vsMaxDestroyValue = "INPUT",
sellSoulbound = "TOGGLE",
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Vendoring.OnInitialize()
TSM.MainUI.Operations.RegisterModule("Vendoring", private.GetVendoringOperationSettings)
end
-- ============================================================================
-- Vendoring Operation Settings UI
-- ============================================================================
function private.GetVendoringOperationSettings(operationName)
TSM.UI.AnalyticsRecordPathChange("main", "operations", "vendoring")
private.currentOperationName = operationName
local operation = TSM.Operations.GetSettings("Vendoring", private.currentOperationName)
return UIElements.New("ScrollFrame", "settings")
:SetPadding(8, 8, 8, 0)
:AddChild(TSM.MainUI.Operations.CreateExpandableSection("Vendoring", "buyOptionsHeading", L["Buy Options"], L["Set what is bought from a vendor."])
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("enableBuy", L["Enable buying"])
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("ToggleOnOff", "toggle")
:SetHeight(18)
:SetSettingInfo(operation, "enableBuy")
:SetDisabled(TSM.Operations.HasRelationship("Vendoring", private.currentOperationName, "enableBuy"))
:SetScript("OnValueChanged", private.EnableBuyingToggleOnValueChanged)
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("restockQty", L["Restock quantity"], not operation.enableBuy)
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Input", "input")
:SetMargin(0, 8, 0, 0)
:SetBackgroundColor("ACTIVE_BG")
:SetValidateFunc("NUMBER", "0:50000")
:SetSettingInfo(operation, "restockQty")
:SetDisabled(TSM.Operations.HasRelationship("Vendoring", private.currentOperationName, "restockQty") or not operation.enableBuy)
)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY3")
:SetTextColor((TSM.Operations.HasRelationship("Vendoring", private.currentOperationName, "restockQty") or not operation.enableBuy) and "TEXT_DISABLED" or "TEXT")
:SetFormattedText(L["Enter a value from %d - %d"], 0, 50000)
)
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("restockSources", L["Sources to include for restock"], not operation.enableBuy)
:SetLayout("VERTICAL")
:SetHeight(48)
:AddChild(UIElements.New("MultiselectionDropdown", "dropdown")
:SetHeight(24)
:SetItems(RESTOCK_SOURCES, RESTOCK_SOURCES_KEYS)
:SetSettingInfo(operation, "restockSources")
:SetSelectionText(L["No Sources"], L["%d Sources"], L["All Sources"])
:SetDisabled(TSM.Operations.HasRelationship("Vendoring", private.currentOperationName, "restockSources") or not operation.enableBuy)
)
)
)
:AddChild(TSM.MainUI.Operations.CreateExpandableSection("Vendoring", "sellOptionsHeading", L["Sell Options"], L["Set what is sold to a vendor."])
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("enableSell", L["Enable selling"])
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("ToggleOnOff", "toggle")
:SetHeight(18)
:SetSettingInfo(operation, "enableSell")
:SetScript("OnValueChanged", private.EnableSellingToggleOnValueChanged)
:SetDisabled(TSM.Operations.HasRelationship("Vendoring", private.currentOperationName, "enableSell"))
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("keepQty", L["Keep quantity"], not operation.enableSell)
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Input", "input")
:SetMargin(0, 8, 0, 0)
:SetBackgroundColor("ACTIVE_BG")
:SetValidateFunc("NUMBER", "0:50000")
:SetSettingInfo(operation, "keepQty")
:SetDisabled(TSM.Operations.HasRelationship("Vendoring", private.currentOperationName, "keepQty") or not operation.enableSell)
)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY3")
:SetTextColor((TSM.Operations.HasRelationship("Vendoring", private.currentOperationName, "keepQty") or not operation.enableSell) and "TEXT_DISABLED" or "TEXT")
:SetFormattedText(L["Enter a value from %d - %d"], 0, 50000)
)
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("sellAfterExpired", L["Min number of expires"], not operation.enableSell)
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Input", "input")
:SetMargin(0, 8, 0, 0)
:SetBackgroundColor("ACTIVE_BG")
:SetValidateFunc("NUMBER", "0:50000")
:SetSettingInfo(operation, "sellAfterExpired")
:SetDisabled(TSM.Operations.HasRelationship("Vendoring", private.currentOperationName, "sellAfterExpired") or not operation.enableSell)
)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY3")
:SetTextColor((TSM.Operations.HasRelationship("Vendoring", private.currentOperationName, "sellAfterExpired") or not operation.enableSell) and "TEXT_DISABLED" or "TEXT")
:SetFormattedText(L["Enter a value from %d - %d"], 0, 50000)
)
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("vsMarketValue", L["Market value"], not operation.enableSell)
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("Input", "input")
:SetHeight(24)
:SetBackgroundColor("ACTIVE_BG")
:SetValidateFunc("CUSTOM_PRICE")
:SetSettingInfo(operation, "vsMarketValue")
:SetDisabled(TSM.Operations.HasRelationship("Vendoring", private.currentOperationName, "vsMarketValue") or not operation.enableSell)
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("vsMaxMarketValue", L["Max market value (Enter '0c' to disable)"], not operation.enableSell)
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("Input", "input")
:SetHeight(24)
:SetBackgroundColor("ACTIVE_BG")
:SetValidateFunc("CUSTOM_PRICE")
:SetSettingInfo(operation, "vsMaxMarketValue")
:SetDisabled(TSM.Operations.HasRelationship("Vendoring", private.currentOperationName, "vsMaxMarketValue") or not operation.enableSell)
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("vsDestroyValue", L["Destroy value"], not operation.enableSell)
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("Input", "input")
:SetHeight(24)
:SetBackgroundColor("ACTIVE_BG")
:SetValidateFunc("CUSTOM_PRICE")
:SetSettingInfo(operation, "vsDestroyValue")
:SetDisabled(TSM.Operations.HasRelationship("Vendoring", private.currentOperationName, "vsDestroyValue") or not operation.enableSell)
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("vsMaxDestroyValue", L["Max destroy value (Enter '0c' to disable)"], not operation.enableSell)
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("Input", "input")
:SetHeight(24)
:SetBackgroundColor("ACTIVE_BG")
:SetValidateFunc("CUSTOM_PRICE")
:SetSettingInfo(operation, "vsMaxDestroyValue")
:SetDisabled(TSM.Operations.HasRelationship("Vendoring", private.currentOperationName, "vsMaxDestroyValue") or not operation.enableSell)
)
)
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine("sellSoulbound", L["Sell soulbound items"], not operation.enableSell)
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 0)
:AddChild(UIElements.New("ToggleOnOff", "toggle")
:SetHeight(18)
:SetSettingInfo(operation, "sellSoulbound")
:SetDisabled(TSM.Operations.HasRelationship("Vendoring", private.currentOperationName, "sellSoulbound") or not operation.enableSell)
)
)
)
:AddChild(TSM.MainUI.Operations.GetOperationManagementElements("Vendoring", private.currentOperationName))
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.UpdateSettingState(settingsFrame, key, operation, value)
local relationshipSet, linkTexture, textColor = TSM.Operations.GetRelationshipColors("Vendoring", private.currentOperationName, key, value)
local settingKeyFrame = settingsFrame:GetElement(key)
settingKeyFrame:GetElement("line.linkBtn")
:SetBackground(linkTexture)
:SetDisabled(not value)
settingKeyFrame:GetElement("line.label")
:SetTextColor(textColor)
local settingType = SETTING_INFO[key]
if settingType == "INPUT_LABEL" then
settingKeyFrame:GetElement("content.input")
:SetDisabled(relationshipSet or not value)
:SetValue(operation[key] or "")
settingKeyFrame:GetElement("content.label")
:SetTextColor(textColor)
elseif settingType == "INPUT" then
settingKeyFrame:GetElement("input")
:SetDisabled(relationshipSet or not value)
elseif settingType == "TOGGLE" then
settingKeyFrame:GetElement("toggle")
:SetDisabled(relationshipSet or not value)
elseif settingType == "DROPDOWN" then
settingKeyFrame:GetElement("dropdown")
:SetDisabled(relationshipSet or not value)
else
error("Invalid settingType: "..tostring(settingType))
end
end
function private.EnableBuyingToggleOnValueChanged(toggle, value)
local operation = TSM.Operations.GetSettings("Vendoring", private.currentOperationName)
local settingsFrame = toggle:GetElement("__parent.__parent")
private.UpdateSettingState(settingsFrame, "restockQty", operation, value)
private.UpdateSettingState(settingsFrame, "restockSources", operation, value)
settingsFrame:Draw()
end
function private.EnableSellingToggleOnValueChanged(toggle, value)
local operation = TSM.Operations.GetSettings("Vendoring", private.currentOperationName)
local settingsFrame = toggle:GetElement("__parent.__parent")
for key in pairs(SETTING_INFO) do
if key ~= "restockQty" and key ~= "restockSources" then
private.UpdateSettingState(settingsFrame, key, operation, value)
end
end
settingsFrame:Draw()
end

View File

@@ -0,0 +1,108 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Warehousing = TSM.MainUI.Operations:NewPackage("Warehousing")
local L = TSM.Include("Locale").GetTable()
local UIElements = TSM.Include("UI.UIElements")
local private = { currentOperationName = nil }
-- ============================================================================
-- Module Functions
-- ============================================================================
function Warehousing.OnInitialize()
TSM.MainUI.Operations.RegisterModule("Warehousing", private.GetWarehousingOperationSettings)
end
-- ============================================================================
-- Warehousing Operation Settings UI
-- ============================================================================
function private.GetWarehousingOperationSettings(operationName)
TSM.UI.AnalyticsRecordPathChange("main", "operations", "warehousing")
private.currentOperationName = operationName
return UIElements.New("ScrollFrame", "settings")
:SetPadding(8, 8, 8, 0)
:AddChild(TSM.MainUI.Operations.CreateExpandableSection("Warehousing", "moveSettings", L["Move Quantity Options"], L["Set how items are moved out of the bank."])
:AddChild(private.CreateEnabledSettingLine("moveQuantity", L["Set move quantity"], L["Quantity to move"], 0, 50000, true))
:AddChild(private.CreateEnabledSettingLine("stackSize", L["Set stack size"], L["Stack size multiple"], 0, 200, true))
:AddChild(private.CreateEnabledSettingLine("keepBagQuantity", L["Set keep in bags quantity"], L["Keep in bags quantity"], 0, 50000, true))
:AddChild(private.CreateEnabledSettingLine("keepBankQuantity", L["Set keep in bank quantity"], L["Keep in bank quantity"], 0, 50000))
)
:AddChild(TSM.MainUI.Operations.CreateExpandableSection("Warehousing", "restockSettings", L["Restock Options"], L["Set how items are restocked from the bank."])
:AddChild(private.CreateEnabledSettingLine("restockQuantity", L["Enable restock"], L["Restock quantity"], 0, 50000, true))
:AddChild(private.CreateEnabledSettingLine("restockStackSize", L["Set stack size for restock"], L["Stack size multiple"], 0, 200, true))
:AddChild(private.CreateEnabledSettingLine("restockKeepBankQuantity", L["Set keep in bank quantity"], L["Keep in bank quantity"], 0, 50000))
)
:AddChild(TSM.MainUI.Operations.GetOperationManagementElements("Warehousing", private.currentOperationName))
end
function private.CreateEnabledSettingLine(key, enableText, text, minValue, maxValue, margin)
local operation = TSM.Operations.GetSettings("Warehousing", private.currentOperationName)
local hasRelationship = TSM.Operations.HasRelationship("Warehousing", private.currentOperationName, key)
return UIElements.New("Frame", "content")
:SetLayout("VERTICAL")
:AddChild(TSM.MainUI.Operations.CreateLinkedSettingLine(key, text)
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("ToggleOnOff", "toggle")
:SetHeight(18)
:SetValue(operation[key] ~= 0)
:SetDisabled(hasRelationship)
:SetContext(key)
:SetScript("OnValueChanged", private.EnabledSettingEnableOnValueChanged)
)
)
:AddChild(TSM.MainUI.Operations.CreateSettingLine("content", text, hasRelationship or operation[key] == 0)
:SetLayout("VERTICAL")
:SetHeight(48)
:SetMargin(0, 0, 0, margin and 12 or 4)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Input", "input")
:SetMargin(0, 8, 0, 0)
:SetBackgroundColor("ACTIVE_BG")
:SetValidateFunc("NUMBER", minValue..":"..maxValue)
:SetSettingInfo(operation, key)
:SetDisabled(hasRelationship or operation[key] == 0)
)
:AddChild(UIElements.New("Text", "label")
:SetWidth("AUTO")
:SetFont("BODY_BODY3")
:SetTextColor((hasRelationship or operation[key] == 0) and "TEXT_DISABLED" or "TEXT")
:SetFormattedText(L["Enter a value from %d - %d"], minValue, maxValue)
)
)
)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.EnabledSettingEnableOnValueChanged(toggle, value)
local key = toggle:GetContext()
local operation = TSM.Operations.GetSettings("Warehousing", private.currentOperationName)
operation[key] = value and 1 or 0
local settingFrame = toggle:GetElement("__parent.__parent.content")
settingFrame:GetElement("label")
:SetTextColor(value and "TEXT" or "TEXT_DISABLED")
settingFrame:GetElement("content.input")
:SetDisabled(not value)
:SetValue(operation[key])
settingFrame:GetElement("content.label")
:SetTextColor(value and "TEXT" or "TEXT_DISABLED")
settingFrame:Draw()
end

View File

@@ -0,0 +1,105 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Accounting = TSM.MainUI.Settings:NewPackage("Accounting")
local L = TSM.Include("Locale").GetTable()
local Log = TSM.Include("Util.Log")
local UIElements = TSM.Include("UI.UIElements")
local private = {}
local DAYS_OLD_OPTIONS = { 0, 15, 30, 45, 60, 75, 90, 180, 360 }
-- ============================================================================
-- Module Functions
-- ============================================================================
function Accounting.OnInitialize()
TSM.MainUI.Settings.RegisterSettingPage(L["Accounting"], "middle", private.GetAccountingSettingsFrame)
end
-- ============================================================================
-- Accounting Settings UI
-- ============================================================================
function private.GetAccountingSettingsFrame()
TSM.UI.AnalyticsRecordPathChange("main", "settings", "accounting")
return UIElements.New("ScrollFrame", "accountingSettings")
:SetPadding(8, 8, 8, 0)
:AddChild(TSM.MainUI.Settings.CreateExpandableSection("Accounting", "accounting", L["General Options"], L["Some general Accounting options are below."])
:AddChild(UIElements.New("Frame", "check1")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("Checkbox", "tradeCheckbox")
:SetWidth("AUTO")
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Track Sales / Purchases via trade"])
:SetSettingInfo(TSM.db.global.accountingOptions, "trackTrades")
)
:AddChild(UIElements.New("Spacer", "spacer"))
)
:AddChild(UIElements.New("Frame", "check2")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:AddChild(UIElements.New("Checkbox", "tradePromptCheckbox")
:SetWidth("AUTO")
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Don't prompt to record trades"])
:SetSettingInfo(TSM.db.global.accountingOptions, "autoTrackTrades")
)
:AddChild(UIElements.New("Spacer", "spacer"))
)
)
:AddChild(TSM.MainUI.Settings.CreateExpandableSection("Accounting", "accounting", L["Clear Old Data"], L["You can clear old Accounting data below to keep things running smoothly."])
:AddChild(UIElements.New("Text", "daysOldLabel")
:SetHeight(20)
:SetMargin(0, 0, 0, 4)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Remove Data Older Than (Days)"])
)
:AddChild(UIElements.New("Frame", "daysOld")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("SelectionDropdown", "dropdown")
:SetMargin(0, 8, 0, 0)
:SetHintText(L["None Selected"])
:SetItems(DAYS_OLD_OPTIONS)
:SetScript("OnSelectionChanged", private.DaysOldDropdownOnSelectionChanged)
)
:AddChild(UIElements.New("ActionButton", "clearBtn")
:SetWidth(107)
:SetDisabled(true)
:SetText(L["Clear Data"])
:SetScript("OnClick", private.ClearBtnOnClick)
)
)
)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.DaysOldDropdownOnSelectionChanged(dropdown)
dropdown:GetElement("__parent.clearBtn")
:SetDisabled(false)
:Draw()
end
function private.ClearBtnOnClick(button)
local days = button:GetElement("__parent.dropdown"):GetSelectedItem()
button:GetBaseElement():ShowConfirmationDialog(L["Clear Old Data?"], L["Are you sure you want to clear old accounting data?"], private.ClearDataConfirmed, days)
end
function private.ClearDataConfirmed(days)
Log.PrintfUser(L["Removed a total of %s old records."], TSM.Accounting.Transactions.RemoveOldData(days) + TSM.Accounting.Money.RemoveOldData(days) + TSM.Accounting.Auctions.RemoveOldData(days))
end

View File

@@ -0,0 +1,182 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Appearance = TSM.MainUI.Settings:NewPackage("Appearance")
local L = TSM.Include("Locale").GetTable()
local Theme = TSM.Include("Util.Theme")
local LibDBIcon = LibStub("LibDBIcon-1.0")
local UIElements = TSM.Include("UI.UIElements")
local private = {
colorSetKeys = {},
colorSetNames = {},
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Appearance.OnInitialize()
for _, key, name in TSM.UI.Util.ColorSetIterator() do
tinsert(private.colorSetKeys, key)
tinsert(private.colorSetNames, name)
end
TSM.MainUI.Settings.RegisterSettingPage(L["Appearance"], "middle", private.GetSettingsFrame)
end
-- ============================================================================
-- Appearance Settings UI
-- ============================================================================
function private.GetSettingsFrame()
TSM.UI.AnalyticsRecordPathChange("main", "settings", "appearance")
return UIElements.New("ScrollFrame", "generalSettings")
:SetPadding(8, 8, 8, 0)
:AddChild(TSM.MainUI.Settings.CreateExpandableSection("Appearance", "appearance", L["General Options"], L["Some general appearance options are below."])
:AddChild(UIElements.New("Frame", "content")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("Checkbox", "minimapCheckbox")
:SetWidth("AUTO")
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Hide minimap icon"])
:SetSettingInfo(TSM.db.global.coreOptions.minimapIcon, "hide")
:SetScript("OnValueChanged", private.MinimapOnValueChanged)
)
:AddChild(UIElements.New("Spacer", "spacer"))
)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("Checkbox", "taskListLockCheckbox")
:SetWidth("AUTO")
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Lock task list's background"])
:SetSettingInfo(TSM.db.global.appearanceOptions, "taskListBackgroundLock")
:SetScript("OnValueChanged", private.TaskListLockOnValueChanged)
)
:AddChild(UIElements.New("Spacer", "spacer"))
)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:AddChild(UIElements.New("Checkbox", "showTotalMoneyCheckbox")
:SetWidth("AUTO")
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Show total gold in header"])
:SetSettingInfo(TSM.db.global.appearanceOptions, "showTotalMoney")
)
:AddChild(UIElements.New("Spacer", "spacer"))
)
)
:AddChild(UIElements.New("Text", "label")
:SetHeight(24)
:SetMargin(12, 0, 4, 12)
:SetFont("BODY_BODY1_BOLD")
:SetText(L["Themes"])
)
:AddChild(UIElements.New("Frame", "theme")
:SetLayout("FLOW")
:AddChildrenWithFunction(private.AddTheme)
)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.MinimapOnValueChanged(_, value)
if value then
LibDBIcon:Hide("TradeSkillMaster")
else
LibDBIcon:Show("TradeSkillMaster")
end
end
function private.TaskListLockOnValueChanged(_, value)
TSM.db.global.appearanceOptions.taskListBackgroundLock = value
if TSM.UI.TaskListUI.IsVisible() then
TSM.UI.TaskListUI.UpdateFrame()
end
end
function private.AddTheme(frame)
for _, key, name in TSM.UI.Util.ColorSetIterator() do
frame:AddChild(UIElements.New("Frame", name)
:SetLayout("VERTICAL")
:SetSize(198, 140)
:SetPadding(0, 0, 12, 8)
:SetMargin(0, 12, 0, 8)
:SetBackgroundColor(Theme.GetColor("FRAME_BG", key), true)
:SetBorderColor(Theme.GetColor("ACTIVE_BG_ALT", key))
:SetContext(key)
:AddChild(UIElements.New("Frame", "top")
:SetLayout("HORIZONTAL")
:SetHeight(36)
:SetMargin(8, 8, 0, 12)
:AddChild(UIElements.New("Frame", "left")
:SetSize(36, 36)
:SetMargin(0, 12, 0, 0)
:SetBackgroundColor(Theme.GetColor("ACTIVE_BG_ALT", key), true)
)
:AddChild(UIElements.New("Frame", "right")
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Frame", "line1")
:SetHeight(12)
:SetMargin(0, 0, 0, 12)
:SetBackgroundColor(Theme.GetColor("ACTIVE_BG", key), true)
)
:AddChild(UIElements.New("Frame", "line2")
:SetHeight(12)
:SetBackgroundColor(Theme.GetColor("PRIMARY_BG_ALT", key), true)
)
)
)
:AddChild(UIElements.New("Frame", "line3")
:SetMargin(8, 8, 0, 12)
:SetBackgroundColor(Theme.GetColor("PRIMARY_BG", key), true)
)
:AddChild(UIElements.New("Texture", "divider")
:SetHeight(1)
:SetTexture(Theme.GetColor("ACTIVE_BG_ALT", key))
)
:AddChild(UIElements.New("Toggle", "toggle")
:SetHeight(20)
:SetMargin(8, 0, 8, 0)
:SetFont("BODY_BODY2_MEDIUM")
:AddOption(Theme.GetThemeName(key), TSM.db.global.appearanceOptions.colorSet == key)
:SetScript("OnValueChanged", private.ThemeButtonOnClick)
)
:AddChildNoLayout(UIElements.New("Button", "btn")
:AddAnchor("TOPLEFT")
:AddAnchor("BOTTOMRIGHT")
:SetScript("OnClick", private.ThemeButtonOnClick)
)
)
end
end
function private.ThemeButtonOnClick(buttonToggle)
local selectedKey = buttonToggle:GetParentElement():GetContext()
for _, key, name in TSM.UI.Util.ColorSetIterator() do
local toggle = buttonToggle:GetElement("__parent.__parent."..name..".toggle")
if key == selectedKey then
toggle:SetOption(name, true)
else
toggle:ClearOption(true)
end
end
TSM.db.global.appearanceOptions.colorSet = selectedKey
Theme.SetActiveColorSet(selectedKey)
end

View File

@@ -0,0 +1,225 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Auctioning = TSM.MainUI.Settings:NewPackage("Auctioning")
local L = TSM.Include("Locale").GetTable()
local Sound = TSM.Include("Util.Sound")
local String = TSM.Include("Util.String")
local Log = TSM.Include("Util.Log")
local UIElements = TSM.Include("UI.UIElements")
local private = {
sounds = {},
soundkeys = {},
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Auctioning.OnInitialize()
TSM.MainUI.Settings.RegisterSettingPage(L["Auctioning"], "middle", private.GetAuctioningSettingsFrame)
for key, name in pairs(Sound.GetSounds()) do
tinsert(private.sounds, name)
tinsert(private.soundkeys, key)
end
end
-- ============================================================================
-- Auctioning Settings UI
-- ============================================================================
function private.GetAuctioningSettingsFrame()
TSM.UI.AnalyticsRecordPathChange("main", "settings", "auctioning")
return UIElements.New("ScrollFrame", "auctioningSettings")
:SetPadding(8, 8, 8, 0)
:AddChild(TSM.MainUI.Settings.CreateExpandableSection("Auctioning", "auctioning", L["General Options"], L["Some general Auctioning options are below."])
:AddChild(UIElements.New("Frame", "check1")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("Checkbox", "cancelBids")
:SetWidth("AUTO")
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Cancel auctions with bids"])
:SetSettingInfo(TSM.db.global.auctioningOptions, "cancelWithBid")
)
:AddChild(UIElements.New("Spacer", "spacer"))
)
:AddChild(UIElements.New("Frame", "check2")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:AddChild(UIElements.New("Checkbox", "invalidPrice")
:SetWidth("AUTO")
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Disable invalid price warnings"])
:SetSettingInfo(TSM.db.global.auctioningOptions, "disableInvalidMsg")
)
:AddChild(UIElements.New("Spacer", "spacer"))
)
)
:AddChild(TSM.MainUI.Settings.CreateExpandableSection("Auctioning", "sounds", L["Auction House Sounds"], L["Setup various sounds that play when doing Auctioning scans."])
:AddChild(UIElements.New("Frame", "labelLine1")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 0, 4)
:AddChild(UIElements.New("Text", "scan")
:SetMargin(0, 12, 0, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Scan complete sound"])
)
:AddChild(UIElements.New("Text", "confirm")
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Confirm complete sound"])
)
)
:AddChild(UIElements.New("Frame", "dropdownLine1")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(0, 0, 0, 12)
:AddChild(UIElements.New("SelectionDropdown", "scanComplete")
:SetMargin(0, 12, 0, 0)
:SetItems(private.sounds, private.soundkeys)
:SetSettingInfo(TSM.db.global.auctioningOptions, "scanCompleteSound")
:SetScript("OnSelectionChanged", private.SoundOnSelectionChanged)
)
:AddChild(UIElements.New("SelectionDropdown", "confirmComplete")
:SetItems(private.sounds, private.soundkeys)
:SetSettingInfo(TSM.db.global.auctioningOptions, "confirmCompleteSound")
:SetScript("OnSelectionChanged", private.SoundOnSelectionChanged)
)
)
:AddChild(UIElements.New("Text", "saleLabel")
:SetHeight(20)
:SetMargin(0, 0, 0, 4)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Auction sale sound"])
)
:AddChild(UIElements.New("SelectionDropdown", "saleDropdown")
:SetHeight(24)
:SetItems(private.sounds, private.soundkeys)
:SetSettingInfo(TSM.db.global.coreOptions, "auctionSaleSound")
:SetScript("OnSelectionChanged", private.SoundOnSelectionChanged)
)
)
:AddChild(TSM.MainUI.Settings.CreateExpandableSection("Auctioning", "whitelist", L["Whitelist"], L["TSM will not undercut any players you add to your whitelist."])
:AddChild(UIElements.New("Text", "matchLabel")
:SetHeight(20)
:SetMargin(0, 0, 0, 4)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Match whitelisted players"])
)
:AddChild(UIElements.New("ToggleOnOff", "matchToggle")
:SetHeight(24)
:SetMargin(0, 0, 0, 12)
:SetSettingInfo(TSM.db.global.auctioningOptions, "matchWhitelist")
)
:AddChild(UIElements.New("Text", "addLabel")
:SetHeight(20)
:SetMargin(0, 0, 0, 4)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Whitelisted characters"])
)
:AddChild(UIElements.New("Input", "newPlayerInput")
:SetHeight(24)
:SetMargin(0, 0, 0, 4)
:SetBackgroundColor("ACTIVE_BG")
:SetHintText(L["Enter player name"])
:SetScript("OnEnterPressed", private.NewPlayerOnEnterPressed)
)
:AddChild(UIElements.New("Frame", "whitelistFrame")
:SetLayout("FLOW")
:SetHeight(60)
:AddChildrenWithFunction(private.AddWhitelistRows)
)
)
end
function private.AddWhitelistRows(containerFrame)
for player in pairs(TSM.db.factionrealm.auctioningOptions.whitelist) do
private.AddWhitelistRow(containerFrame, player)
end
end
function private.AddWhitelistRow(frame, player)
frame:AddChild(UIElements.New("Frame", "whitelist_"..player)
:SetLayout("HORIZONTAL")
:SetSize(100, 20)
:SetMargin(0, 12, 0, 0)
:AddChild(UIElements.New("Text", "text")
:SetWidth("AUTO")
:SetMargin(0, 2, 0, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(player)
)
:AddChild(UIElements.New("Button", "removeBtn")
:SetBackgroundAndSize("iconPack.14x14/Close/Circle")
:SetContext(player)
:SetScript("OnClick", private.RemoveWhitelistOnClick)
)
:AddChild(UIElements.New("Spacer", "spacer"))
)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.SoundOnSelectionChanged(self)
Sound.PlaySound(self:GetSelectedItemKey())
end
function private.NewPlayerOnEnterPressed(input)
local newPlayer = strlower(input:GetValue())
input:SetValue("")
input:Draw()
if newPlayer == "" or strfind(newPlayer, ",") or newPlayer ~= String.Escape(newPlayer) then
Log.PrintfUser(L["Invalid player name."])
return
elseif TSM.db.factionrealm.auctioningOptions.whitelist[newPlayer] then
Log.PrintfUser(L["The player \"%s\" is already on your whitelist."], TSM.db.factionrealm.auctioningOptions.whitelist[newPlayer])
return
end
local isAlt = false
for factionrealm in TSM.db:GetConnectedRealmIterator("factionrealm") do
for _, character in TSM.db:FactionrealmCharacterIterator(factionrealm) do
if strlower(newPlayer) == strlower(character) then
Log.PrintfUser(L["You do not need to add \"%s\", alts are whitelisted automatically."], newPlayer)
isAlt = true
end
end
end
if isAlt then
return
end
TSM.db.factionrealm.auctioningOptions.whitelist[newPlayer] = newPlayer
-- add a new row to the UI
local frame = input:GetElement("__parent.whitelistFrame")
private.AddWhitelistRow(frame, newPlayer)
frame:Draw()
end
function private.RemoveWhitelistOnClick(self)
local player = self:GetContext()
TSM.db.factionrealm.auctioningOptions.whitelist[player] = nil
-- remove this row
local row = self:GetParentElement()
local frame = row:GetParentElement()
frame:RemoveChild(row)
row:Release()
frame:Draw()
end

View File

@@ -0,0 +1,287 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Settings = TSM.MainUI:NewPackage("Settings")
local L = TSM.Include("Locale").GetTable()
local Wow = TSM.Include("Util.Wow")
local UIElements = TSM.Include("UI.UIElements")
local private = {
settingPages = {
top = {},
middle = {},
bottom = {},
},
callback = {},
childSettingsPages = {},
sectionCollapsed = {},
}
local SECTIONS = { "top", "middle" }
local SETTING_PATH_SEP = "`"
local SETTING_LABEL_WIDTH = 400
-- ============================================================================
-- Module Functions
-- ============================================================================
function Settings.OnInitialize()
TSM.MainUI.RegisterTopLevelPage(L["Settings"], private.GetSettingsFrame)
end
function Settings.RegisterSettingPage(name, section, callback)
assert(tContains(SECTIONS, section))
tinsert(private.settingPages[section], name)
private.callback[name] = callback
end
function Settings.RegisterChildSettingPage(parentName, childName, callback)
local path = parentName..SETTING_PATH_SEP..childName
private.childSettingsPages[parentName] = private.childSettingsPages[parentName] or {}
tinsert(private.childSettingsPages[parentName], childName)
private.callback[path] = callback
end
function Settings.CreateSettingLine(id, labelText, width)
width = width or SETTING_LABEL_WIDTH
return UIElements.New("Frame", id)
:SetLayout("HORIZONTAL")
:SetHeight(26)
:SetMargin(0, 0, 0, 16)
:AddChild(UIElements.New("Text", "label")
:SetWidth(width)
:SetFont("BODY_BODY2_MEDIUM")
:SetTextColor("TEXT_ALT")
:SetText(labelText)
)
end
function Settings.CreateHeading(id, text)
return UIElements.New("Text", id)
:SetHeight(19)
:SetMargin(0, 0, 0, 4)
:SetFont("BODY_BODY1_BOLD")
:SetText(text)
end
function Settings.CreateInputWithReset(id, label, context, validate)
local scope, namespace, key = strsplit(".", context)
local validateFunc, validateContext = nil, nil
if type(validate) == "table" then
validateFunc = "CUSTOM_PRICE"
validateContext = validate
elseif type(validate) == "function" then
validateFunc = validate
elseif validate == nil then
validateFunc = "CUSTOM_PRICE"
else
error("Invalid validate: "..tostring(validate))
end
return UIElements.New("Frame", id)
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Text", "label")
:SetHeight(18)
:SetMargin(0, 0, 0, 4)
:SetFont("BODY_BODY2_MEDIUM")
:SetTextColor("TEXT_ALT")
:SetText(label)
)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Input", "input")
:SetMargin(0, 8, 0, 0)
:SetBackgroundColor("ACTIVE_BG")
:SetValidateFunc(validateFunc, validateContext)
:SetSettingInfo(TSM.db[scope][namespace], key)
:SetScript("OnValueChanged", private.InputOnValueChanged)
)
:AddChild(UIElements.New("ActionButton", "resetButton")
:SetWidth(108)
:SetText(L["Reset"])
:SetDisabled(TSM.db[scope][namespace][key] == TSM.db:GetDefault(scope, namespace, key))
:SetScript("OnClick", private.ResetBtnOnClick)
:SetContext(context)
)
)
end
function Settings.CreateExpandableSection(pageName, id, text, description, descriptionHeight)
return UIElements.New("CollapsibleContainer", id)
:SetLayout("VERTICAL")
:SetMargin(0, 0, 0, 8)
:SetContextTable(private.sectionCollapsed, pageName..text)
:SetHeadingText(text)
:AddChild(UIElements.New("Text", "description")
:SetHeight(descriptionHeight or 20)
:SetMargin(0, 0, 0, 12)
:SetFont("BODY_BODY3")
:SetText(description)
)
end
function Settings.PromptToReload()
StaticPopupDialogs["TSMReloadPrompt"] = StaticPopupDialogs["TSMReloadPrompt"] or {
text = L["You must reload your UI for these settings to take effect. Reload now?"],
button1 = YES,
button2 = NO,
timeout = 0,
OnAccept = ReloadUI,
}
Wow.ShowStaticPopupDialog("TSMReloadPrompt")
end
-- ============================================================================
-- Settings UI
-- ============================================================================
function private.GetSettingsFrame()
TSM.UI.AnalyticsRecordPathChange("main", "settings")
local defaultPage = private.settingPages.top[1]
local frame = UIElements.New("Frame", "settings")
:SetLayout("HORIZONTAL")
:SetBackgroundColor("PRIMARY_BG_ALT")
:AddChild(UIElements.New("Frame", "settingNavigation")
:SetLayout("VERTICAL")
:SetWidth(160)
:SetPadding(12, 12, 1, 9)
:AddChild(UIElements.New("Frame", "top")
:SetLayout("VERTICAL")
)
:AddChild(UIElements.New("Texture", "vline")
:SetHeight(1)
:SetMargin(0, 0, 8, 8)
:SetTexture("ACTIVE_BG_ALT")
)
:AddChild(UIElements.New("Frame", "middle")
:SetLayout("VERTICAL")
)
:AddChild(UIElements.New("Spacer", "spacer")
-- make all the navigation align to the top
)
)
:AddChild(UIElements.New("Texture", "divider")
:SetWidth(2)
:SetTexture("ACTIVE_BG")
)
:AddChild(UIElements.New("Frame", "contentFrame")
:SetLayout("VERTICAL")
:SetBackgroundColor("PRIMARY_BG")
:AddChild(UIElements.New("ViewContainer", "content")
:SetNavCallback(private.ContentNavCallback)
)
)
local content = frame:GetElement("contentFrame.content")
local settingNav = frame:GetElement("settingNavigation")
for _, location in ipairs(SECTIONS) do
local navFrame = settingNav:GetElement(location)
for _, settingName in ipairs(private.settingPages[location]) do
navFrame:AddChild(UIElements.New("Button", settingName)
:SetHeight(20)
:SetMargin(0, 0, 8, 0)
:SetFont("BODY_BODY2_BOLD")
:SetJustifyH("LEFT")
:SetContext(settingName)
:SetText(settingName)
:SetScript("OnClick", private.NavButtonOnClick)
)
content:AddPath(settingName, settingName == defaultPage)
if private.childSettingsPages[settingName] then
for _, childSettingName in ipairs(private.childSettingsPages[settingName]) do
local path = settingName..SETTING_PATH_SEP..childSettingName
navFrame:AddChild(UIElements.New("Button", path)
:SetHeight(20)
:SetMargin(9, 0, 8, 0)
:SetFont("BODY_BODY3_MEDIUM")
:SetJustifyH("LEFT")
:SetContext(path)
:SetText(strupper(childSettingName))
:SetScript("OnClick", private.NavButtonOnClick)
)
content:AddPath(path, path == defaultPage)
end
end
end
end
private.UpdateNavFrame(settingNav, defaultPage)
return frame
end
function private.ContentNavCallback(content, path)
return private.callback[path]()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.NavButtonOnClick(button)
local path = button:GetContext()
if private.childSettingsPages[path] then
-- select the first child
path = path..SETTING_PATH_SEP..private.childSettingsPages[path][1]
end
local contentFrame = button:GetElement("__parent.__parent.__parent.contentFrame")
local navFrame = contentFrame:GetElement("__parent.settingNavigation")
private.UpdateNavFrame(navFrame, path)
navFrame:Draw()
contentFrame:GetElement("content"):SetPath(path, true)
end
function private.InputOnValueChanged(input)
local button = input:GetElement("__parent.resetButton")
local scope, namespace, key = strsplit(".", button:GetContext())
button:SetDisabled(TSM.db[scope][namespace][key] == TSM.db:GetDefault(scope, namespace, key))
:Draw()
end
function private.ResetBtnOnClick(button)
local scope, namespace, key = strsplit(".", button:GetContext())
local defaultValue = TSM.db:GetDefault(scope, namespace, key)
TSM.db:Set(scope, nil, namespace, key, defaultValue)
button:GetElement("__parent.input")
:SetValue(defaultValue)
:Draw()
button:SetDisabled(true)
:Draw()
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.UpdateNavFrame(navFrame, selectedPath)
local selectedSetting = strsplit(SETTING_PATH_SEP, selectedPath)
for _, location in ipairs(SECTIONS) do
for _, settingName in ipairs(private.settingPages[location]) do
navFrame:GetElement(location ..".".. settingName)
:SetTextColor(settingName == selectedSetting and "TEXT" or "ACTIVE_BG_ALT")
if private.childSettingsPages[settingName] then
for _, childSettingName in ipairs(private.childSettingsPages[settingName]) do
local path = settingName..SETTING_PATH_SEP..childSettingName
if settingName == selectedSetting then
navFrame:GetElement(location ..".".. path)
:SetTextColor(path == selectedPath and "INDICATOR" or "TEXT")
:Show()
else
navFrame:GetElement(location ..".".. path):Hide()
end
end
end
end
end
end

View File

@@ -0,0 +1,142 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Crafting = TSM.MainUI.Settings:NewPackage("Crafting")
local L = TSM.Include("Locale").GetTable()
local PlayerInfo = TSM.Include("Service.PlayerInfo")
local UIElements = TSM.Include("UI.UIElements")
local private = {
altCharacters = {},
altGuilds = {},
}
local BAD_MAT_PRICE_SOURCES = {
matprice = true,
}
local BAD_CRAFT_VALUE_PRICE_SOURCES = {
crafting = true,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Crafting.OnInitialize()
TSM.MainUI.Settings.RegisterSettingPage(L["Crafting"], "middle", private.GetCraftingSettingsFrame)
end
-- ============================================================================
-- Crafting Settings UI
-- ============================================================================
function private.GetCraftingSettingsFrame()
TSM.UI.AnalyticsRecordPathChange("main", "settings", "crafting")
wipe(private.altCharacters)
wipe(private.altGuilds)
for _, character in PlayerInfo.CharacterIterator(true) do
tinsert(private.altCharacters, character)
end
for name in PlayerInfo.GuildIterator() do
tinsert(private.altGuilds, name)
end
return UIElements.New("ScrollFrame", "craftingSettings")
:SetPadding(8, 8, 8, 0)
:AddChild(TSM.MainUI.Settings.CreateExpandableSection("Crafting", "inventory", L["Inventory Options"], "")
:AddChild(UIElements.New("Frame", "inventoryOptionsLabels")
:SetLayout("HORIZONTAL")
:SetMargin(0, 0, 0, 4)
:SetHeight(20)
:AddChild(UIElements.New("Text", "label")
:SetMargin(0, 12, 0, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Ignore Characters"])
)
:AddChild(UIElements.New("Text", "label")
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Ignore Guilds"])
)
)
:AddChild(UIElements.New("Frame", "inventoryOptionsDropdowns")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("MultiselectionDropdown", "charDropdown")
:SetMargin(0, 12, 0, 0)
:SetItems(private.altCharacters, private.altCharacters)
:SetSettingInfo(TSM.db.global.craftingOptions, "ignoreCharacters")
:SetSelectionText(L["No Characters"], L["%d Characters"], L["All Characters"])
)
:AddChild(UIElements.New("MultiselectionDropdown", "guildDropdown")
:SetItems(private.altGuilds, private.altGuilds)
:SetSettingInfo(TSM.db.global.craftingOptions, "ignoreGuilds")
:SetSelectionText(L["No Guilds"], L["%d Guilds"], L["All Guilds"])
)
)
)
:AddChild(TSM.MainUI.Settings.CreateExpandableSection("Crafting", "price", L["Default price configuration"], "")
:AddChild(UIElements.New("Text", "matCostLabel")
:SetHeight(20)
:SetMargin(0, 0, 0, 4)
:SetFont("BODY_BODY2_MEDIUM")
:SetTextColor("TEXT_ALT")
:SetText(L["Default material cost method"])
)
:AddChild(UIElements.New("MultiLineInput", "matCostInput")
:SetHeight(70)
:SetMargin(0, 0, 0, 12)
:SetValidateFunc("CUSTOM_PRICE", BAD_MAT_PRICE_SOURCES)
:SetSettingInfo(TSM.db.global.craftingOptions, "defaultMatCostMethod")
)
:AddChild(UIElements.New("Text", "craftValueLabel")
:SetHeight(20)
:SetMargin(0, 0, 0, 4)
:SetFont("BODY_BODY2_MEDIUM")
:SetTextColor("TEXT_ALT")
:SetText(L["Default craft value method"])
)
:AddChild(UIElements.New("MultiLineInput", "matCostInput")
:SetHeight(70)
:SetValidateFunc("CUSTOM_PRICE", BAD_CRAFT_VALUE_PRICE_SOURCES)
:SetSettingInfo(TSM.db.global.craftingOptions, "defaultCraftPriceMethod")
)
)
:AddChild(TSM.MainUI.Settings.CreateExpandableSection("Crafting", "cooldowns", L["Ignored Cooldowns"], L["Use this list to manage what cooldowns you'd like TSM to ignore from crafting."])
:AddChild(UIElements.New("QueryScrollingTable", "items")
:SetHeight(126)
:GetScrollingTableInfo()
:NewColumn("item")
:SetTitle(L["Cooldown"])
:SetFont("BODY_BODY3")
:SetJustifyH("LEFT")
:SetTextInfo(nil, private.CooldownGetText)
:DisableHiding()
:Commit()
:Commit()
:SetQuery(TSM.Crafting.CreateIgnoredCooldownQuery())
:SetAutoReleaseQuery(true)
:SetSelectionDisabled(true)
:SetScript("OnRowClick", private.IgnoredCooldownOnRowClick)
)
)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.CooldownGetText(row)
return row:GetField("characterKey").." - "..TSM.Crafting.GetName(row:GetField("spellId"))
end
function private.IgnoredCooldownOnRowClick(_, row)
TSM.Crafting.RemoveIgnoredCooldown(row:GetFields("characterKey", "spellId"))
end

View File

@@ -0,0 +1,297 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local CustomSources = TSM.MainUI.Settings:NewPackage("CustomSources")
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
local Theme = TSM.Include("Util.Theme")
local CustomPrice = TSM.Include("Service.CustomPrice")
local UIElements = TSM.Include("UI.UIElements")
local private = {}
-- ============================================================================
-- Module Functions
-- ============================================================================
function CustomSources.OnInitialize()
TSM.MainUI.Settings.RegisterSettingPage(L["Custom Sources"], "middle", private.GetCustomSourcesSettingsFrame)
end
-- ============================================================================
-- Custom Sources Settings UI
-- ============================================================================
function private.GetCustomSourcesSettingsFrame()
TSM.UI.AnalyticsRecordPathChange("main", "settings", "custom_sources")
return UIElements.New("ScrollFrame", "content")
:SetPadding(8, 8, 8, 0)
:AddChild(TSM.MainUI.Settings.CreateExpandableSection("Custom Price", "general", L["Custom Sources"], format(L["Custom sources allow you to create more advanced prices for use throughout the addon. You'll be able to use these new variables in the same way you can use the built-in price sources such as %s and %s."], Theme.GetColor("INDICATOR"):ColorText("vendorsell"), Theme.GetColor("INDICATOR"):ColorText("vendorbuy")), 60)
:AddChild(UIElements.New("Frame", "tableHeading")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:AddChild(UIElements.New("Text", "col1")
:SetWidth(162)
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["Name"])
)
:AddChild(UIElements.New("Text", "col2")
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["String"])
)
)
:AddChild(UIElements.New("Texture", "line1")
:SetHeight(1)
:SetTexture("ACTIVE_BG")
)
:AddChildrenWithFunction(private.AddCustomPriceRows)
:AddChild(UIElements.New("ActionButton", "addNewBtn")
:SetHeight(24)
:SetMargin(0, 0, 32, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Add a new custom source"])
:SetScript("OnClick", private.AddNewButtonOnClick)
)
)
end
function private.CreateCustomPriceRow(name)
local priceString = TSM.db.global.userData.customPriceSources[name]
local row = UIElements.New("Frame", "row_"..name)
:SetLayout("HORIZONTAL")
:SetHeight(28)
:SetMargin(-12, -12, 0, 0)
:SetPadding(12, 12, 0, 0)
:SetBackgroundColor("PRIMARY_BG_ALT")
:SetContext(name)
:SetScript("OnEnter", private.CustomPriceRowOnEnter)
:SetScript("OnLeave", private.CustomPriceRowOnLeave)
:AddChild(UIElements.New("Text", "nameText")
:SetWidth(162)
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY3_MEDIUM")
:SetText(name)
)
:AddChild(UIElements.New("Text", "valueText")
:SetFont("BODY_BODY3")
:SetText(priceString)
)
:AddChild(UIElements.New("Button", "editBtn")
:SetMargin(4, 0, 0, 0)
:SetBackgroundAndSize("iconPack.18x18/Edit")
:SetScript("OnClick", private.EditCustomPriceOnClick)
:PropagateScript("OnEnter")
:PropagateScript("OnLeave")
)
:AddChild(UIElements.New("Button", "deleteBtn")
:SetMargin(4, 0, 0, 0)
:SetBackgroundAndSize("iconPack.18x18/Delete")
:SetScript("OnClick", private.DeleteCustomPriceOnClick)
:PropagateScript("OnEnter")
:PropagateScript("OnLeave")
)
row:GetElement("editBtn"):Hide()
row:GetElement("deleteBtn"):Hide()
return row
end
function private.AddCustomPriceRows(frame)
local names = TempTable.Acquire()
for name in pairs(TSM.db.global.userData.customPriceSources) do
tinsert(names, name)
end
sort(names)
for _, name in ipairs(names) do
frame:AddChild(private.CreateCustomPriceRow(name))
end
TempTable.Release(names)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.CustomPriceRowOnEnter(frame)
frame:SetBackgroundColor("FRAME_BG")
frame:GetElement("editBtn"):Show()
frame:GetElement("deleteBtn"):Show()
frame:Draw()
end
function private.CustomPriceRowOnLeave(frame)
frame:SetBackgroundColor("PRIMARY_BG_ALT")
frame:GetElement("editBtn"):Hide()
frame:GetElement("deleteBtn"):Hide()
frame:Draw()
end
function private.EditCustomPriceOnClick(button)
private.ShowEditDialog(button)
end
function private.ShowEditDialog(editBtn)
local dialogFrame = UIElements.New("Frame", "frame")
:SetLayout("VERTICAL")
:SetSize(478, 314)
:SetPadding(12)
:AddAnchor("CENTER")
:SetMouseEnabled(true)
:SetBackgroundColor("FRAME_BG", true)
:SetContext(editBtn)
:AddChild(UIElements.New("Frame", "header")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(0, 0, -4, 14)
:AddChild(UIElements.New("Spacer", "spacer")
:SetWidth(20)
)
:AddChild(UIElements.New("Text", "title")
:SetJustifyH("CENTER")
:SetFont("BODY_BODY1_BOLD")
:SetText(L["Edit Custom Source"])
)
:AddChild(UIElements.New("Button", "closeBtn")
:SetMargin(0, -4, 0, 0)
:SetBackgroundAndSize("iconPack.24x24/Close/Default")
:SetScript("OnClick", private.EditPriceCloseBtnOnClick)
)
)
:AddChild(UIElements.New("Text", "name")
:SetHeight(20)
:SetFont("BODY_BODY2_MEDIUM")
:SetJustifyH("LEFT")
:SetText(L["Name"])
)
:AddChild(UIElements.New("Input", "nameInput")
:SetHeight(24)
:SetBackgroundColor("PRIMARY_BG_ALT")
:SetValue(editBtn:GetElement("__parent.nameText"):GetText())
:SetTabPaths("__parent.valueInput", "__parent.valueInput")
:SetValidateFunc(private.NameValidateFunc)
:SetScript("OnValidationChanged", private.ConfirmOnValidationChanged)
)
:AddChild(UIElements.New("Text", "string")
:SetHeight(20)
:SetMargin(0, 0, 6, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetJustifyH("LEFT")
:SetText(L["String"])
)
:AddChild(UIElements.New("MultiLineInput", "valueInput")
:SetBackgroundColor("PRIMARY_BG_ALT")
:SetValue(editBtn:GetElement("__parent.valueText"):GetText())
:SetTabPaths("__parent.nameInput", "__parent.nameInput")
:SetValidateFunc(private.ValueValidateFunc)
:SetScript("OnValidationChanged", private.ConfirmOnValidationChanged)
)
:AddChild(UIElements.New("ActionButton", "confirm")
:SetHeight(24)
:SetMargin(0, 0, 12, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(L["Confirm"])
:SetContext(editBtn:GetElement("__parent.__parent"))
:SetScript("OnClick", private.ConfirmOnClick)
)
editBtn:GetBaseElement():ShowDialogFrame(dialogFrame)
return dialogFrame
end
function private.EditPriceCloseBtnOnClick(button)
button:GetBaseElement():HideDialog()
end
function private.NameValidateFunc(input, value)
if value == "" then
return false
elseif gsub(value, "([a-z]+)", "") ~= "" then
return false, L["Custom price names can only contain lowercase letters."]
elseif value ~= input:GetParentElement():GetContext():GetParentElement():GetContext() then
return CustomPrice.ValidateName(value)
end
return true
end
function private.ConfirmOnValidationChanged(input)
input:GetElement("__parent.confirm")
:SetDisabled(not input:IsValid())
:Draw()
end
function private.ValueValidateFunc(input, value)
value = strlower(strtrim(value))
local isValid, errMsg = CustomPrice.Validate(value)
if not isValid and value ~= "" then
return false, errMsg
end
return true
end
function private.ConfirmOnClick(button)
local baseElement = button:GetBaseElement()
local oldName = button:GetParentElement():GetContext():GetParentElement():GetContext()
local newName = button:GetElement("__parent.nameInput"):GetValue()
if oldName ~= newName then
CustomPrice.RenameCustomPriceSource(oldName, newName)
CustomPrice.SetCustomPriceSource(newName, button:GetElement("__parent.valueInput"):GetValue())
local generalContainer = button:GetParentElement():GetContext():GetParentElement():GetParentElement()
local rowFrame = button:GetParentElement():GetContext():GetParentElement()
generalContainer:AddChildBeforeById("addNewBtn", private.CreateCustomPriceRow(newName))
generalContainer:RemoveChild(rowFrame)
rowFrame:Release()
generalContainer:GetElement("__parent.__parent")
:Draw()
else
CustomPrice.SetCustomPriceSource(newName, button:GetElement("__parent.valueInput"):GetValue())
button:GetParentElement():GetContext():GetElement("__parent.nameText")
:SetText(newName)
:Draw()
button:GetParentElement():GetContext():GetElement("__parent.valueText")
:SetText(button:GetElement("__parent.valueInput"):GetValue())
:Draw()
end
baseElement:HideDialog()
end
function private.DeleteCustomPriceOnClick(button)
CustomPrice.DeleteCustomPriceSource(button:GetParentElement():GetContext())
local rowFrame = button:GetParentElement()
local parentFrame = rowFrame:GetParentElement()
parentFrame:RemoveChild(rowFrame)
rowFrame:Release()
parentFrame:GetElement("__parent.__parent")
:Draw()
end
function private.AddNewButtonOnClick(button)
-- generate a placeholder name
local newName = nil
local suffix = ""
while not newName do
for i = strbyte("a"), strbyte("z") do
newName = "customprice"..suffix..strchar(i)
if not TSM.db.global.userData.customPriceSources[newName] then
break
end
newName = nil
end
suffix = suffix..strchar(random(strbyte("a"), strbyte("z")))
end
CustomPrice.CreateCustomPriceSource(newName, "")
button:GetParentElement()
:AddChildBeforeById("addNewBtn", private.CreateCustomPriceRow(newName))
button:GetElement("__parent.__parent.__parent")
:Draw()
local dialogFrame = private.ShowEditDialog(button:GetElement("__parent.row_"..newName..".editBtn"))
dialogFrame:GetElement("valueInput"):SetFocused(true)
end

Some files were not shown because too many files have changed in this diff Show More