--========================================================-- -- Scorpio Secure Group Panel -- -- -- -- Author : kurapica125@outlook.com -- -- Create Date : 2020/11/14 -- --========================================================-- --========================================================-- Scorpio "Scorpio.Secure.SecureGroupPanel" "1.0.0" --========================================================-- ----------------------------------------------------------- -- Secure Element Panel Widget -- ----------------------------------------------------------- __Sealed__() class "SecureGroupPanel" (function(_ENV) inherit "SecurePanel" __Sealed__() enum "GroupType" { "NONE", "GROUP", "CLASS", "ROLE", "ASSIGNEDROLE" } __Sealed__() enum "SortType" { "INDEX", "NAME" } __Sealed__() enum "RoleType" { "MAINTANK", "MAINASSIST", "TANK", "HEALER", "DAMAGER", "NONE" } __Sealed__() enum "PlayerClass" { "WARRIOR", "DEATHKNIGHT", "PALADIN", "MONK", "PRIEST", "SHAMAN", "DRUID", "ROGUE", "MAGE", "WARLOCK", "HUNTER", "DEMONHUNTER" } __Sealed__() struct "GroupFilter" { System.Number } __Sealed__() struct "ClassFilter" { PlayerClass } __Sealed__() struct "RoleFilter" { RoleType } DEFAULT_CLASS_SORT_ORDER = { "WARRIOR", "DEATHKNIGHT", "PALADIN", "MONK", "PRIEST", "SHAMAN", "DRUID", "ROGUE", "MAGE", "WARLOCK", "HUNTER", "DEMONHUNTER" } DEFAULT_ROLE_SORT_ORDER = { "MAINTANK", "MAINASSIST", "TANK", "HEALER", "DAMAGER", "NONE"} DEFAULT_GROUP_SORT_ORDER = { 1, 2, 3, 4, 5, 6, 7, 8 } -- A shadow refresh system based on the SecureGroupHeaderTemplate __SecureTemplate__() __Sealed__() class "ShadowGroupHeader" (function(_ENV) inherit "SecureFrame" export { min = math.min } -- Delay Refresh Manager _DelayManagerFrame = SecureFrame("Scorpio_SecureGroupPanel_RefreshMananger", UIParent, "SecureHandlerStateTemplate") _DelayManagerFrame:Hide() _DelayManagerFrame:Execute([=[ Manager = self _Panel = newtable() _Queue = newtable() Manager:SetAttribute("registerDelayAction", [[ local panel, action = ... if panel and action then _Queue[panel] = action -- Reset the timer Manager:SetAttribute("state-timer", "reset") end ]]) ]=]) -- The condition has no real use, just a timer ticker _DelayManagerFrame:SetAttribute("_onstate-timer", [=[ if newstate ~= "reset" then for panel, action in pairs(_Queue) do _Panel[panel]:RunAttribute(action) end wipe(_Queue) end ]=]) _DelayManagerFrame:RegisterStateDriver("timer", "[pet]pet;nopet;") -- The Secure Panel Snippets _InitHeader = [=[ Manager = self DelayManager = self:GetFrameRef("DelayManager") UnitFrames = newtable() ShadowFrames = newtable() DeadFrames = newtable() ShadowUnitMap = newtable() Manager:SetAttribute("refreshDeadPlayer", [[ for i = 1, #DeadFrames do local unitFrame = UnitFrames[i] if unitFrame then unitFrame:SetAttribute("unit", DeadFrames[i]:GetAttribute("unit")) else break end end for i = #DeadFrames + 1, #UnitFrames do local unitFrame = UnitFrames[i] if unitFrame:GetAttribute("unit") then unitFrame:SetAttribute("unit", nil) else break end end ]]) Manager:SetAttribute("newDeadPlayer", [[ local id = ... if id then -- Follow the order for i = 1, #DeadFrames + 1 do if DeadFrames[i] then local oid = DeadFrames[i]:GetID() if oid > id then tinsert(DeadFrames, i, ShadowFrames[id]) break elseif oid == id then break end else tinsert(DeadFrames, i, ShadowFrames[id]) end end return DelayManager:RunAttribute("registerDelayAction", Manager:GetAttribute("PanelName"), "refreshDeadPlayer") end ]]) Manager:SetAttribute("removeDeadPlayer", [[ local id = ... if id then local sfrm = ShadowFrames[id] for i = 1, #DeadFrames do if DeadFrames[i] == sfrm then tremove(DeadFrames, i) return DelayManager:RunAttribute("registerDelayAction", Manager:GetAttribute("PanelName"), "refreshDeadPlayer") end end end ]]) Manager:SetAttribute("updateStateForChild", [[ local id = ... if id then local shadow= ShadowFrames[id] local unit = shadow:GetAttribute("unit") UnregisterAttributeDriver(shadow, "isdead") shadow:SetAttribute("isdead", nil) if unit then RegisterAttributeDriver(shadow, "isdead", format("[@%s,dead]true;false;", unit)) end end ]]) Manager:SetAttribute("refreshUnitFrames", [[ local count = #ShadowFrames for i = 1, count do local frm = UnitFrames[i] if not frm then return end frm:SetAttribute("unit", ShadowFrames[i]:GetAttribute("unit")) end for i = count + 1, #UnitFrames do UnitFrames[i]:SetAttribute("unit", nil) end ]]) Manager:SetAttribute("onShadowUnitChanged", [[ local id, unit = ... if ShadowUnitMap[id] ~= unit then ShadowUnitMap[id] = unit DelayManager:RunAttribute("registerDelayAction", Manager:GetAttribute("PanelName"), "refreshUnitFrames") end ]]) refreshUnitChange = [[ local unit = self:GetAttribute("unit") local frame = self:GetAttribute("UnitFrame") if frame then self:GetAttribute("Manager"):RunAttribute("onShadowUnitChanged", self:GetID(), value) elseif self:GetAttribute("Manager"):GetAttribute("showDeadOnly") then self:GetAttribute("Manager"):RunAttribute("removeDeadPlayer", self:GetID()) self:GetAttribute("Manager"):RunAttribute("updateStateForChild", self:GetID()) end ]] ]=] _Onattributechanged = [[ if name == "unit" then if type(value) == "string" then value = strlower(value) else value = nil end local frame = self:GetAttribute("UnitFrame") if frame then self:GetAttribute("Manager"):RunAttribute("onShadowUnitChanged", self:GetID(), value) elseif self:GetAttribute("Manager"):GetAttribute("showDeadOnly") then self:GetAttribute("Manager"):RunAttribute("removeDeadPlayer", self:GetID()) self:GetAttribute("Manager"):RunAttribute("updateStateForChild", self:GetID()) end elseif name == "isdead" then if value == "true" then self:GetAttribute("Manager"):RunAttribute("newDeadPlayer", self:GetID()) else self:GetAttribute("Manager"):RunAttribute("removeDeadPlayer", self:GetID()) end end ]] _InitialConfigFunction = [=[ tinsert(ShadowFrames, self) self:SetWidth(0) self:SetHeight(0) self:SetID(#ShadowFrames) self:SetAttribute("Manager", Manager) -- Binding local frame = UnitFrames[#ShadowFrames] if frame and not Manager:GetAttribute("showDeadOnly") then self:SetAttribute("UnitFrame", frame) end -- Only for the entering game combat -- refreshUnitChange won't fire when the unit is set to nil self:SetAttribute("refreshUnitChange", refreshUnitChange) Manager:CallMethod("UpdateUnitCount", #ShadowFrames) ]=] _RegisterUnitFrame = [=[ local frame = Manager:GetFrameRef("UnitFrame") if frame then tinsert(UnitFrames, frame) -- Binding if not Manager:GetAttribute("showDeadOnly") then local shadow = ShadowFrames[#UnitFrames] if shadow then shadow:SetAttribute("UnitFrame", frame) frame:SetAttribute("unit", shadow:GetAttribute("unit")) end else if #DeadFrames >= #UnitFrames then frame:SetAttribute("unit", DeadFrames[#UnitFrames]:GetAttribute("unit")) end end end ]=] _Hide = [[ for i = #ShadowFrames, 1, -1 do ShadowFrames[i]:SetAttribute("unit", nil) end -- Make sure hide all unit frames for i = #UnitFrames, 1, -1 do UnitFrames[i]:SetAttribute("unit", nil) end ]] _ToggleShowOnlyPlayer = [[ wipe(ShadowUnitMap) if self:GetAttribute("showDeadOnly") then for i = 1, #ShadowFrames do ShadowFrames[i]:SetAttribute("UnitFrame", nil) end for i = 1, #UnitFrames do UnitFrames[i]:SetAttribute("unit", nil) end for i = 1, #ShadowFrames do self:RunAttribute("updateStateForChild", i) end else wipe(DeadFrames) for i = 1, #ShadowFrames do local shadow = ShadowFrames[i] local frame = UnitFrames[i] UnregisterAttributeDriver(shadow, "isdead") if frame then ShadowFrames[i]:SetAttribute("UnitFrame", frame) frame:SetAttribute("unit", ShadowFrames[i]:GetAttribute("unit")) end end for i = #ShadowFrames + 1, #UnitFrames do UnitFrames[i]:SetAttribute("unit", nil) end end ]] ------------------------------------------------------ -- Method ------------------------------------------------------ __SecureMethod__() __AsyncSingle__(true) function UpdateUnitCount(self, count) Next() NoCombat() -- Init the panel local panel = self:GetParent() count = min(count, panel.MaxCount) -- Limit the count for i = self.__InitedCount + 1, count do -- Init the child local child = self:GetAttribute("child" .. i) child:SetAttribute("refreshUnitChange", nil) -- only used for the entering game combat child:SetAttribute("isdead", nil) child:SetAttribute("_onattributechanged", _Onattributechanged) end self.__InitedCount = count if panel and count and panel.Count < count then panel.Count = count end end -- The default refresh method function Refresh(self) if self:IsShown() and not InCombatLockdown() then -- Well, it's ugly but useful to trigger refreshing self:Hide() self:Show() end end -- Register an unit frame function RegisterUnitFrame(self, frame) self:SetFrameRef("UnitFrame", frame) self:Execute(_RegisterUnitFrame) end -- Set whether only show dead players __NoCombat__() function SetShowDeadOnly(self, flag) flag = flag and true or false if flag ~= self:IsShowDeadOnly() then self:SetAttribute("showDeadOnly", flag) self:Execute(_ToggleShowOnlyPlayer) end end -- Whether only show the dead players function IsShowDeadOnly(self) return self:GetAttribute("showDeadOnly") and true or false end ------------------------------------------------------ -- Property ------------------------------------------------------ -- Whether only show the dead players property "ShowDeadOnly" { get = IsShowDeadOnly, set = SetShowDeadOnly, type = Boolean, } ------------------------------------------------------ -- Constructor ------------------------------------------------------ function __ctor(self, ...) self.__InitedCount = 0 self:SetFrameRef("DelayManager", _DelayManagerFrame) self:Execute(_InitHeader) self:SetAttribute("PanelName", self:GetParent():GetName()) _DelayManagerFrame:SetFrameRef("GroupPanel", self) _DelayManagerFrame:Execute([[ local panel = self:GetFrameRef("GroupPanel") _Panel[panel:GetAttribute("PanelName")] = panel ]]) self:SetAttribute("template", "SecureHandlerAttributeTemplate") self:SetAttribute("initialConfigFunction", _InitialConfigFunction) self:SetAttribute("strictFiltering", true) self:SetAttribute("groupingOrder", "") self:WrapScript(self, "OnHide", _Hide) -- Throw out of the screen self:SetPoint("TOPRIGHT", UIParent, "TOPLEFT") self:SetAlpha(0) end end) ------------------------------------------------------ -- Helper functions ------------------------------------------------------ local _Cache = {} local function secureSetAttribute(self, attr, value) return NoCombat(self.SetAttribute, self, attr, value) end local function setupGroupFilter(self) local groupFilter = self.GroupFilter or DEFAULT_GROUP_SORT_ORDER local classFilter = self.ClassFilter or DEFAULT_CLASS_SORT_ORDER wipe(_Cache) for _, v in ipairs(groupFilter) do tinsert(_Cache, v) end for _, v in ipairs(classFilter) do tinsert(_Cache, v) end secureSetAttribute(self.GroupHeader, "groupFilter", tblconcat(_Cache, ",")) wipe(_Cache) end local function setupRoleFilter(self) local roleFilter = self.RoleFilter or DEFAULT_ROLE_SORT_ORDER wipe(_Cache) for _, v in ipairs(roleFilter) do tinsert(_Cache, v) end secureSetAttribute(self.GroupHeader, "roleFilter", tblconcat(_Cache, ",")) wipe(_Cache) end local function setupGroupingOrder(self) local groupBy = self.GroupBy local filter if groupBy == "GROUP" then filter = self.GroupFilter or DEFAULT_GROUP_SORT_ORDER elseif groupBy == "CLASS" then filter = self.ClassFilter or DEFAULT_CLASS_SORT_ORDER elseif groupBy == "ROLE" or groupBy == "ASSIGNEDROLE" then filter = self.RoleFilter or DEFAULT_ROLE_SORT_ORDER end wipe(_Cache) if filter then for _, v in ipairs(filter) do tinsert(_Cache, v) end end secureSetAttribute(self.GroupHeader, "groupingOrder", tblconcat(_Cache, ",")) wipe(_Cache) end ------------------------------------------------------ -- Method ------------------------------------------------------ -- The default refresh method function Refresh(self) if InCombatLockdown() then return end self.GroupHeader:Refresh() return self:RefreshLayout() end function SetAutoHide(self, value) return self.GroupHeader:SetAutoHide(value) end ------------------------------------------------------ -- Property ------------------------------------------------------ -- Whether the panel should be shown while in a raid property "ShowRaid" { get = function(self) return self.GroupHeader:GetAttribute("showRaid") end, set = function(self, value) secureSetAttribute(self.GroupHeader, "showRaid", value) end, type = Boolean, } -- Whether the panel should be shown while in a party and not in a raid property "ShowParty" { get = function(self) return self.GroupHeader:GetAttribute("showParty") end, set = function(self, value) secureSetAttribute(self.GroupHeader, "showParty", value) end, type = Boolean, } -- Whether the panel should show the player while not in a raid property "ShowPlayer" { get = function(self) return self.GroupHeader:GetAttribute("showPlayer") end, set = function(self, value) secureSetAttribute(self.GroupHeader, "showPlayer", value) end, type = Boolean, } -- Whether the panel should be shown while not in a group property "ShowSolo" { get = function(self) return self.GroupHeader:GetAttribute("showSolo") end, set = function(self, value) secureSetAttribute(self.GroupHeader, "showSolo", value) end, type = Boolean, } -- A list of raid group numbers, used as the filter settings and order settings(if GroupBy is "GROUP") __Set__(PropertySet.Clone) property "GroupFilter" { handler = function (self, value) setupGroupFilter(self) if self.GroupBy == "GROUP" then setupGroupingOrder(self) end end, type = GroupFilter, } -- A list of uppercase class names, used as the filter settings and order settings(if GroupBy is "CLASS") __Set__(PropertySet.Clone) property "ClassFilter" { handler = function (self, value) setupGroupFilter(self) if self.GroupBy == "CLASS" then setupGroupingOrder(self) end end, type = ClassFilter, } -- A list of uppercase role names, used as the filter settings and order settings(if GroupBy is "ROLE") __Set__(PropertySet.Clone) property "RoleFilter" { handler = function (self, value) setupRoleFilter(self) if self.GroupBy == "ROLE" or self.GroupBy == "ASSIGNEDROLE" then setupGroupingOrder(self) end end, type = RoleFilter, } -- Specifies a "grouping" type to apply before regular sorting (Default: nil) property "GroupBy" { handler = function (self, value) if value == "NONE" then value = nil end secureSetAttribute(self.GroupHeader, "groupBy", value) setupGroupingOrder(self) end, type = GroupType, default = "NONE", } -- Defines how the group is sorted (Default: "INDEX") property "SortBy" { get = function(self) return self.GroupHeader:GetAttribute("sortMethod") end, set = function(self, value) secureSetAttribute(self.GroupHeader, "sortMethod", value) end, type = SortType, default = "INDEX", } -- The group header based on the blizzard's SecureGroupHeader property "GroupHeader" { default = function(self) return ShadowGroupHeader(self:GetName() .. "ShadowGroupHeader", self, "SecureGroupHeaderTemplate") end, type = ShadowGroupHeader } -- Whether only show the dead players property "ShowDeadOnly" { get = function(self) return self.GroupHeader.ShowDeadOnly end, set = function(self, value) self.GroupHeader.ShowDeadOnly = value end, type = Boolean, } ------------------------------------------------------ -- Event Handler ------------------------------------------------------ local function OnElementAdd(self, element) element.UnitWatchEnabled = false element:SetAttribute("unit", nil) --element:InstantApplyStyle() self.GroupHeader:RegisterUnitFrame(element) end ------------------------------------------------------ -- Constructor ------------------------------------------------------ function __ctor(self, ...) super(self, ...) Next(function(self) while true do if not self:IsShown() then Next(Observable.From(self.OnShow)) else Next(Wow.FromEvent("GROUP_ROSTER_UPDATE")) end Delay(0.2) -- Just enough NoCombat() if self:IsShown() then self.GroupHeader:Refresh() end end end, self) self.OnElementAdd = self.OnElementAdd + OnElementAdd setupGroupFilter(self) end end) __Sealed__() class "SecureGroupPetPanel" (function(_ENV) inherit "SecureGroupPanel" ------------------------------------------------------ -- Property ------------------------------------------------------ -- if true, then pet names are used when sorting the list property "FilterOnPet" { get = function(self) return self.GroupHeader:GetAttribute("filterOnPet") or false end, set = function(self, value) NoCombat(self.GroupHeader.SetAttribute, self.GroupHeader, "filterOnPet", value) end, type = Boolean, } property "GroupHeader" { default = function(self) return SecureGroupPanel.ShadowGroupHeader(self:GetName() .. "ShadowGroupHeader", self, "SecureGroupPetHeaderTemplate") end, } end) ----------------------------------------------------------- -- Default Style -- ----------------------------------------------------------- Style.UpdateSkin("Default", { [SecureGroupPanel] = { rowCount = _G.MEMBERS_PER_RAID_GROUP or 5, columnCount = _G.NUM_RAID_GROUPS or 8, autoHide = CLEAR, elementWidth = 80, elementHeight = 32, orientation = "VERTICAL", leftToRight = true, topToBottom = true, hSpacing = 2, vSpacing = 2, marginTop = 0, marginBottom = 0, marginLeft = 0, marginRight = 0, showRaid = true, showParty = true, showSolo = true, showPlayer = true, }, [SecureGroupPetPanel] = { showRaid = false, showParty = true, showSolo = true, showPlayer = true, }, })