-- ------------------------------------------------------------------------------ -- -- 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