Thread Tools Display Modes
03-03-12, 05:38 AM   #1
MiRai
A Warpwood Thunder Caller
Join Date: Jul 2011
Posts: 96
oUF_CustomLayouts

While I was asking questions specifically about oUF_Karma before in my other thread, I felt that it would probably be better if I started a new thread for this question since it seems like it would pertain to oUF overall and not just a specific layout.

Currently, from what I understand, oUF can only sort party members using one of the following methods:
Code:
"sortMethod", "INDEX",
"sortMethod", "NAME",
What I would like to ask is... If someone would be willing to design an oUF plug-in similar to GridCustomLayouts. What GridCustomLayouts does is let you set a list of names so that you can manipulate the raid or party frames to show the names that you want.

I ask because when I join a battleground with my friends, we usually end up in different groups and it can be difficult to heal them through the raid frames when we're scattered about. I would like to "hardcode" their names into my party list so that I can just click heal them through there. Grid paired up with GridCustomLayouts can achieve this but I would really like to not have to use Grid just for this and would like to keep everything consolidated and neat within the oUF frames.

In the other thread, Phanx said:
Originally Posted by Phanx
You would need to filter on a nameList instead of groups or classes. See the comments in Blizzard's FrameXML/SecureGroupHeaders.lua file for more detail, or someone not on a phone can post it for you

Here's the SecureGroupHeaders.lua from go-hero.net for 4.3.0.15005:

lua Code:
  1. --
  2. -- SecurePartyHeader and SecureRaidGroupHeader contributed with permission by: Esamynn, Cide, and Iriel
  3. --
  4.  
  5. local strsplit = strsplit;
  6. local select = select;
  7. local tonumber = tonumber;
  8. local type = type;
  9. local floor = math.floor;
  10. local ceil = math.ceil;
  11. local min = math.min;
  12. local max = math.max;
  13. local abs = math.abs;
  14.  
  15. --[[
  16. List of the various configuration attributes
  17. ======================================================
  18. showRaid = [BOOLEAN] -- true if the header should be shown while in a raid
  19. showParty = [BOOLEAN] -- true if the header should be shown while in a party and not in a raid
  20. showPlayer = [BOOLEAN] -- true if the header should show the player when not in a raid
  21. showSolo = [BOOLEAN] -- true if the header should be shown while not in a group (implies showPlayer)
  22. nameList = [STRING] -- a comma separated list of player names (not used if 'groupFilter' is set)
  23. groupFilter = [1-8, STRING] -- a comma seperated list of raid group numbers and/or uppercase class names and/or uppercase roles
  24. strictFiltering = [BOOLEAN] - if true, then characters must match both a group and a class from the groupFilter list
  25. point = [STRING] -- a valid XML anchoring point (Default: "TOP")
  26. xOffset = [NUMBER] -- the x-Offset to use when anchoring the unit buttons (Default: 0)
  27. yOffset = [NUMBER] -- the y-Offset to use when anchoring the unit buttons (Default: 0)
  28. sortMethod = ["INDEX", "NAME", "NAMELIST"] -- defines how the group is sorted (Default: "INDEX")
  29. sortDir = ["ASC", "DESC"] -- defines the sort order (Default: "ASC")
  30. template = [STRING] -- the XML template to use for the unit buttons
  31. templateType = [STRING] - specifies the frame type of the managed subframes (Default: "Button")
  32. groupBy = [nil, "GROUP", "CLASS", "ROLE"] - specifies a "grouping" type to apply before regular sorting (Default: nil)
  33. groupingOrder = [STRING] - specifies the order of the groupings (ie. "1,2,3,4,5,6,7,8")
  34. maxColumns = [NUMBER] - maximum number of columns the header will create (Default: 1)
  35. unitsPerColumn = [NUMBER or nil] - maximum units that will be displayed in a singe column, nil is infinite (Default: nil)
  36. startingIndex = [NUMBER] - the index in the final sorted unit list at which to start displaying units (Default: 1)
  37. columnSpacing = [NUMBER] - the amount of space between the rows/columns (Default: 0)
  38. columnAnchorPoint = [STRING] - the anchor point of each new column (ie. use LEFT for the columns to grow to the right)
  39. --]]
  40.  
  41. function SecureGroupHeader_OnLoad(self)
  42.     self:RegisterEvent("PARTY_MEMBERS_CHANGED");
  43.     self:RegisterEvent("UNIT_NAME_UPDATE");
  44. end
  45.  
  46. function SecureGroupHeader_OnEvent(self, event, ...)
  47.     if ( (event == "PARTY_MEMBERS_CHANGED" or event == "UNIT_NAME_UPDATE") and self:IsVisible() ) then
  48.         SecureGroupHeader_Update(self);
  49.     end
  50. end
  51.  
  52. function SecureGroupHeader_OnAttributeChanged(self, name, value)
  53.     if ( name == "_ignore" or self:GetAttribute("_ignore" ) ) then
  54.         return
  55.     end
  56.     if ( self:IsVisible() ) then
  57.         SecureGroupHeader_Update(self);
  58.     end
  59. end
  60.  
  61. -- relativePoint, xMultiplier, yMultiplier = getRelativePointAnchor( point )
  62. -- Given a point return the opposite point and which axes the point
  63. -- depends on.
  64. local function getRelativePointAnchor( point )
  65.     point = point:upper();
  66.     if (point == "TOP") then
  67.         return "BOTTOM", 0, -1;
  68.     elseif (point == "BOTTOM") then
  69.         return "TOP", 0, 1;
  70.     elseif (point == "LEFT") then
  71.         return "RIGHT", 1, 0;
  72.     elseif (point == "RIGHT") then
  73.         return "LEFT", -1, 0;
  74.     elseif (point == "TOPLEFT") then
  75.         return "BOTTOMRIGHT", 1, -1;
  76.     elseif (point == "TOPRIGHT") then
  77.         return "BOTTOMLEFT", -1, -1;
  78.     elseif (point == "BOTTOMLEFT") then
  79.         return "TOPRIGHT", 1, 1;
  80.     elseif (point == "BOTTOMRIGHT") then
  81.         return "TOPLEFT", -1, 1;
  82.     else
  83.         return "CENTER", 0, 0;
  84.     end
  85. end
  86.  
  87. local function setAttributesWithoutResponse(self, ...)
  88.     local oldIgnore = self:GetAttribute("_ignore");
  89.     self:SetAttribute("_ignore", "attributeChanges");
  90.     for i = 1, select('#', ...), 2 do
  91.         self:SetAttribute(select(i, ...));
  92.     end
  93.     self:SetAttribute("_ignore", oldIgnore);
  94. end
  95.  
  96. local CallRestrictedClosure = CallRestrictedClosure;
  97. local GetManagedEnvironment = GetManagedEnvironment;
  98. local GetFrameHandle = GetFrameHandle;
  99. local wipe = table.wipe;
  100. local tinsert = table.insert;
  101.  
  102. local function SetupUnitButtonConfiguration( header, newChild, defaultConfigFunction )
  103.     local configCode = header:GetAttribute("initialConfigFunction") or defaultConfigFunction;
  104.  
  105.     if ( type(configCode) == "string" ) then
  106.         local selfHandle = GetFrameHandle(newChild);
  107.         if ( selfHandle ) then
  108.             CallRestrictedClosure("self", GetManagedEnvironment(header, true),
  109.                                   selfHandle, configCode, selfHandle);
  110.         end
  111.     end
  112. end
  113.  
  114. -- creates child frames and finished configuring them
  115. local function configureChildren(self, unitTable)
  116.     local point = self:GetAttribute("point") or "TOP"; --default anchor point of "TOP"
  117.     local relativePoint, xOffsetMult, yOffsetMult = getRelativePointAnchor(point);
  118.     local xMultiplier, yMultiplier =  abs(xOffsetMult), abs(yOffsetMult);
  119.     local xOffset = self:GetAttribute("xOffset") or 0; --default of 0
  120.     local yOffset = self:GetAttribute("yOffset") or 0; --default of 0
  121.     local sortDir = self:GetAttribute("sortDir") or "ASC"; --sort ascending by default
  122.     local columnSpacing = self:GetAttribute("columnSpacing") or 0;
  123.     local startingIndex = self:GetAttribute("startingIndex") or 1;
  124.  
  125.     local unitCount = #unitTable;
  126.     local numDisplayed = unitCount - (startingIndex - 1);
  127.     local unitsPerColumn = self:GetAttribute("unitsPerColumn");
  128.     local numColumns;
  129.     if ( unitsPerColumn and numDisplayed > unitsPerColumn ) then
  130.         numColumns = min( ceil(numDisplayed / unitsPerColumn), (self:GetAttribute("maxColumns") or 1) );
  131.     else
  132.         unitsPerColumn = numDisplayed;
  133.         numColumns = 1;
  134.     end
  135.     local loopStart = startingIndex;
  136.     local loopFinish = min((startingIndex - 1) + unitsPerColumn * numColumns, unitCount)
  137.     local step = 1;
  138.  
  139.     numDisplayed = loopFinish - (loopStart - 1);
  140.  
  141.     if ( sortDir == "DESC" ) then
  142.         loopStart = unitCount - (startingIndex - 1);
  143.         loopFinish = loopStart - (numDisplayed - 1);
  144.         step = -1;
  145.     end
  146.  
  147.     -- ensure there are enough buttons
  148.     local needButtons = max(1, numDisplayed);
  149.     if not ( self:GetAttribute("child"..needButtons) ) then
  150.         local buttonTemplate = self:GetAttribute("template");
  151.         local templateType = self:GetAttribute("templateType") or "Button";
  152.         local name = self:GetName();
  153.         for i = 1, needButtons, 1 do
  154.             local childAttr = "child" .. i;
  155.             if not ( self:GetAttribute(childAttr) ) then
  156.                 local newButton = CreateFrame(templateType, name and (name.."UnitButton"..i), self, buttonTemplate);
  157.                 self[i] = newButton;
  158.                 SetupUnitButtonConfiguration(self, newButton);
  159.                 setAttributesWithoutResponse(self, childAttr, newButton, "frameref-"..childAttr, GetFrameHandle(newButton));
  160.             end
  161.         end
  162.     end
  163.  
  164.     local columnAnchorPoint, columnRelPoint, colxMulti, colyMulti;
  165.     if ( numColumns > 1 ) then
  166.         columnAnchorPoint = self:GetAttribute("columnAnchorPoint");
  167.         columnRelPoint, colxMulti, colyMulti = getRelativePointAnchor(columnAnchorPoint);
  168.     end
  169.  
  170.     local buttonNum = 0;
  171.     local columnNum = 1;
  172.     local columnUnitCount = 0;
  173.     local currentAnchor = self;
  174.     for i = loopStart, loopFinish, step do
  175.         buttonNum = buttonNum + 1;
  176.         columnUnitCount = columnUnitCount + 1;
  177.         if ( columnUnitCount > unitsPerColumn ) then
  178.             columnUnitCount = 1;
  179.             columnNum = columnNum + 1;
  180.         end
  181.  
  182.         local unitButton = self:GetAttribute("child"..buttonNum);
  183.         if ( buttonNum == 1 ) then
  184.             unitButton:SetPoint(point, currentAnchor, point, 0, 0);
  185.             if ( columnAnchorPoint ) then
  186.                 unitButton:SetPoint(columnAnchorPoint, currentAnchor, columnAnchorPoint, 0, 0);
  187.             end
  188.  
  189.         elseif ( columnUnitCount == 1 ) then
  190.             local columnAnchor = self:GetAttribute("child"..(buttonNum - unitsPerColumn));
  191.             unitButton:SetPoint(columnAnchorPoint, columnAnchor, columnRelPoint, colxMulti * columnSpacing, colyMulti * columnSpacing);
  192.         else
  193.             unitButton:SetPoint(point, currentAnchor, relativePoint, xMultiplier * xOffset, yMultiplier * yOffset);
  194.         end
  195.         unitButton:SetAttribute("unit", unitTable[i]);
  196.  
  197.         local configCode = unitButton:GetAttribute("refreshUnitChange");
  198.         if ( type(configCode) == "string" ) then
  199.             local selfHandle = GetFrameHandle(unitButton);
  200.             if ( selfHandle ) then
  201.                 CallRestrictedClosure("self",
  202.                                       GetManagedEnvironment(unitButton, true),
  203.                                       selfHandle, configCode, selfHandle);
  204.             end
  205.         end
  206.  
  207.         if not unitButton:GetAttribute("statehidden") then
  208.             unitButton:Show();
  209.         end
  210.  
  211.         currentAnchor = unitButton;
  212.     end
  213.     repeat
  214.         buttonNum = buttonNum + 1;
  215.         local unitButton = self:GetAttribute("child"..buttonNum);
  216.         if ( unitButton ) then
  217.             unitButton:Hide();
  218.             unitButton:ClearAllPoints();
  219.             unitButton:SetAttribute("unit", nil);
  220.         end
  221.     until not ( unitButton )
  222.  
  223.     local unitButton = self:GetAttribute("child1");
  224.     local unitButtonWidth = unitButton:GetWidth();
  225.     local unitButtonHeight = unitButton:GetHeight();
  226.     if ( numDisplayed > 0 ) then
  227.         local width = xMultiplier * (unitsPerColumn - 1) * unitButtonWidth + ( (unitsPerColumn - 1) * (xOffset * xOffsetMult) ) + unitButtonWidth;
  228.         local height = yMultiplier * (unitsPerColumn - 1) * unitButtonHeight + ( (unitsPerColumn - 1) * (yOffset * yOffsetMult) ) + unitButtonHeight;
  229.  
  230.         if ( numColumns > 1 ) then
  231.             width = width + ( (numColumns -1) * abs(colxMulti) * (width + columnSpacing) );
  232.             height = height + ( (numColumns -1) * abs(colyMulti) * (height + columnSpacing) );
  233.         end
  234.  
  235.         self:SetWidth(width);
  236.         self:SetHeight(height);
  237.     else
  238.         local minWidth = self:GetAttribute("minWidth") or (yMultiplier * unitButtonWidth);
  239.         local minHeight = self:GetAttribute("minHeight") or (xMultiplier * unitButtonHeight);
  240.         self:SetWidth( max(minWidth, 0.1) );
  241.         self:SetHeight( max(minHeight, 0.1) );
  242.     end
  243. end
  244.  
  245. local function GetGroupHeaderType(self)
  246.     local kind, start, stop;
  247.  
  248.     local nRaid = GetNumRaidMembers();
  249.     local nParty = GetNumPartyMembers();
  250.     if ( nRaid > 0 and self:GetAttribute("showRaid") ) then
  251.         kind = "RAID";
  252.     elseif ( (nRaid > 0 or nParty > 0) and self:GetAttribute("showParty") ) then
  253.         kind = "PARTY";
  254.     elseif ( self:GetAttribute("showSolo") ) then
  255.         kind = "SOLO";
  256.     end
  257.     if ( kind ) then
  258.         if ( kind == "RAID" ) then
  259.             start = 1;
  260.             stop = nRaid;
  261.         else
  262.             if ( kind == "SOLO" or self:GetAttribute("showPlayer") ) then
  263.                 start = 0;
  264.             else
  265.                 start = 1;
  266.             end
  267.             stop = nParty;
  268.         end
  269.     end
  270.     return kind, start, stop;
  271. end
  272.  
  273. local function GetGroupRosterInfo(kind, index)
  274.     local _, unit, name, subgroup, className, role, server;
  275.     if ( kind == "RAID" ) then
  276.         unit = "raid"..index;
  277.         name, _, subgroup, _, _, className, _, _, _, role = GetRaidRosterInfo(index);
  278.     else
  279.         if ( index > 0 ) then
  280.             unit = "party"..index;
  281.         else
  282.             unit = "player";
  283.         end
  284.         if ( UnitExists(unit) ) then
  285.             name, server = UnitName(unit);
  286.             if (server and server ~= "") then
  287.                 name = name.."-"..server
  288.             end
  289.             _, className = UnitClass(unit);
  290.             if ( GetPartyAssignment("MAINTANK", unit) ) then
  291.                 role = "MAINTANK";
  292.             elseif ( GetPartyAssignment("MAINASSIST", unit) ) then
  293.                 role = "MAINASSIST";
  294.             end
  295.         end
  296.         subgroup = 1;
  297.     end
  298.     return unit, name, subgroup, className, role;
  299. end
  300.  
  301. local pairs = pairs;
  302. local ipairs = ipairs;
  303. local strtrim = string.trim;
  304.  
  305. -- empties tbl and assigns the value true to each key passed as part of ...
  306. local function fillTable( tbl, ... )
  307.     for i = 1, select("#", ...), 1 do
  308.         local key = select(i, ...);
  309.         key = tonumber(key) or strtrim(key);
  310.         tbl[key] = i;
  311.     end
  312. end
  313.  
  314. -- same as fillTable() except that each key is also stored in
  315. -- the array portion of the table in order
  316. local function doubleFillTable( tbl, ... )
  317.     fillTable(tbl, ...);
  318.     for i = 1, select("#", ...), 1 do
  319.         tbl[i] = strtrim(select(i, ...));
  320.     end
  321. end
  322.  
  323. --working tables
  324. local tokenTable = {};
  325. local sortingTable = {};
  326. local groupingTable = {};
  327. local tempTable = {};
  328.  
  329. local function sortOnGroupWithNames(a, b)
  330.     local order1 = tokenTable[ groupingTable[a] ];
  331.     local order2 = tokenTable[ groupingTable[b] ];
  332.     if ( order1 ) then
  333.         if ( not order2 ) then
  334.             return true;
  335.         else
  336.             if ( order1 == order2 ) then
  337.                 return sortingTable[a] < sortingTable[b];
  338.             else
  339.                 return order1 < order2;
  340.             end
  341.         end
  342.     else
  343.         if ( order2 ) then
  344.             return false;
  345.         else
  346.             return sortingTable[a] < sortingTable[b];
  347.         end
  348.     end
  349. end
  350.  
  351. local function sortOnGroupWithIDs(a, b)
  352.     local order1 = tokenTable[ groupingTable[a] ];
  353.     local order2 = tokenTable[ groupingTable[b] ];
  354.     if ( order1 ) then
  355.         if ( not order2 ) then
  356.             return true;
  357.         else
  358.             if ( order1 == order2 ) then
  359.                 return tonumber(a:match("%d+") or -1) < tonumber(b:match("%d+") or -1);
  360.             else
  361.                 return order1 < order2;
  362.             end
  363.         end
  364.     else
  365.         if ( order2 ) then
  366.             return false;
  367.         else
  368.             return tonumber(a:match("%d+") or -1) < tonumber(b:match("%d+") or -1);
  369.         end
  370.     end
  371. end
  372.  
  373. local function sortOnNames(a, b)
  374.     return sortingTable[a] < sortingTable[b];
  375. end
  376.  
  377. local function sortOnNameList(a, b)
  378.     return tokenTable[ sortingTable[a] ] < tokenTable[ sortingTable[b] ];
  379. end
  380.  
  381. function SecureGroupHeader_Update(self)
  382.     local nameList = self:GetAttribute("nameList");
  383.     local groupFilter = self:GetAttribute("groupFilter");
  384.     local sortMethod = self:GetAttribute("sortMethod");
  385.     local groupBy = self:GetAttribute("groupBy");
  386.  
  387.     wipe(sortingTable);
  388.  
  389.     -- See if this header should be shown
  390.     local kind, start, stop = GetGroupHeaderType(self);
  391.     if ( not kind ) then
  392.         configureChildren(self, sortingTable);
  393.         return;
  394.     end
  395.  
  396.     if ( not groupFilter and not nameList ) then
  397.         groupFilter = "1,2,3,4,5,6,7,8";
  398.     end
  399.  
  400.     if ( groupFilter ) then
  401.         -- filtering by a list of group numbers and/or classes
  402.         fillTable(wipe(tokenTable), strsplit(",", groupFilter));
  403.         local strictFiltering = self:GetAttribute("strictFiltering"); -- non-strict by default
  404.         for i = start, stop, 1 do
  405.             local unit, name, subgroup, className, role = GetGroupRosterInfo(kind, i);
  406.             if ( name and
  407.                 ((not strictFiltering) and
  408.                     (tokenTable[subgroup] or tokenTable[className] or (role and tokenTable[role])) -- non-strict filtering
  409.                 ) or
  410.                     (tokenTable[subgroup] and tokenTable[className]) -- strict filtering
  411.             ) then
  412.                 tinsert(sortingTable, unit);
  413.                 sortingTable[unit] = name;
  414.                 if ( groupBy == "GROUP" ) then
  415.                     groupingTable[unit] = subgroup;
  416.  
  417.                 elseif ( groupBy == "CLASS" ) then
  418.                     groupingTable[unit] = className;
  419.  
  420.                 elseif ( groupBy == "ROLE" ) then
  421.                     groupingTable[unit] = role;
  422.  
  423.                 end
  424.             end
  425.         end
  426.  
  427.         if ( groupBy ) then
  428.             local groupingOrder = self:GetAttribute("groupingOrder");
  429.             doubleFillTable(wipe(tokenTable), strsplit(",", groupingOrder:gsub("%s+", "")));
  430.             if ( sortMethod == "NAME" ) then
  431.                 table.sort(sortingTable, sortOnGroupWithNames);
  432.             else
  433.                 table.sort(sortingTable, sortOnGroupWithIDs);
  434.             end
  435.         elseif ( sortMethod == "NAME" ) then -- sort by ID by default
  436.             table.sort(sortingTable, sortOnNames);
  437.         end
  438.  
  439.     else
  440.         -- filtering via a list of names
  441.         doubleFillTable(wipe(tokenTable), strsplit(",", nameList));
  442.         for i = start, stop, 1 do
  443.             local unit, name = GetGroupRosterInfo(kind, i);
  444.             if ( tokenTable[name] ) then
  445.                 tinsert(sortingTable, unit);
  446.                 sortingTable[unit] = name;
  447.             end
  448.         end
  449.         if ( sortMethod == "NAME" ) then
  450.             table.sort(sortingTable, sortOnNames);
  451.         elseif ( sortMethod == "NAMELIST" ) then
  452.             table.sort(sortingTable, sortOnNameList)
  453.         end
  454.  
  455.     end
  456.  
  457.     configureChildren(self, sortingTable);
  458. end
  459.  
  460. --[[
  461. The Pet Header accepts all of the various configuration attributes of the
  462. regular raid header, as well as the following
  463. ======================================================
  464. useOwnerUnit = [BOOLEAN] - if true, then the owner's unit string is set on managed frames "unit" attribute (instead of pet's)
  465. filterOnPet = [BOOLEAN] - if true, then pet names are used when sorting/filtering the list
  466. --]]
  467.  
  468. function SecureGroupPetHeader_OnLoad(self)
  469.     self:RegisterEvent("PARTY_MEMBERS_CHANGED");
  470.     self:RegisterEvent("UNIT_NAME_UPDATE");
  471.     self:RegisterEvent("UNIT_PET");
  472. end
  473.  
  474. function SecureGroupPetHeader_OnEvent(self, event, ...)
  475.     if ( (event == "PARTY_MEMBERS_CHANGED" or event == "UNIT_NAME_UPDATE" or event == "UNIT_PET") and self:IsVisible() ) then
  476.         SecureGroupPetHeader_Update(self);
  477.     end
  478. end
  479.  
  480. function SecureGroupPetHeader_OnAttributeChanged(self, name, value)
  481.     if ( name == "_ignore" or self:GetAttribute("_ignore" ) ) then
  482.         return
  483.     end
  484.     if ( self:IsVisible() ) then
  485.         SecureGroupPetHeader_Update(self);
  486.     end
  487. end
  488.  
  489. local function GetPetUnit(kind, index)
  490.     if ( kind == "RAID" ) then
  491.         return "raidpet"..index;
  492.     elseif ( index > 0 ) then
  493.         return "partypet"..index;
  494.     else
  495.         return "pet";
  496.     end
  497. end
  498.  
  499. function SecureGroupPetHeader_Update(self)
  500.     local nameList = self:GetAttribute("nameList");
  501.     local groupFilter = self:GetAttribute("groupFilter");
  502.     local sortMethod = self:GetAttribute("sortMethod");
  503.     local groupBy = self:GetAttribute("groupBy");
  504.     local useOwnerUnit = self:GetAttribute("useOwnerUnit");
  505.     local filterOnPet = self:GetAttribute("filterOnPet");
  506.  
  507.     wipe(sortingTable);
  508.  
  509.     -- See if this header should be shown
  510.     local kind, start, stop = GetGroupHeaderType(self);
  511.     if ( not kind ) then
  512.         configureChildren(self, sortingTable);
  513.         return;
  514.     end
  515.  
  516.     if ( not groupFilter and not nameList ) then
  517.         groupFilter = "1,2,3,4,5,6,7,8";
  518.     end
  519.  
  520.     if ( groupFilter ) then
  521.         -- filtering by a list of group numbers and/or classes
  522.         fillTable(wipe(tokenTable), strsplit(",", groupFilter));
  523.         local strictFiltering = self:GetAttribute("strictFiltering"); -- non-strict by default
  524.         for i = start, stop, 1 do
  525.             local unit, name, subgroup, className, role = GetGroupRosterInfo(kind, i);
  526.             local petUnit = GetPetUnit(kind, i);
  527.             if ( filterOnPet ) then
  528.                 name = UnitName(petUnit);
  529.             end
  530.             if not ( useOwnerUnit ) then
  531.                 unit = petUnit;
  532.             end
  533.             if ( UnitExists(petUnit) ) then
  534.                 if ( name and
  535.                     ((not strictFiltering) and
  536.                      (tokenTable[subgroup] or tokenTable[className] or (role and tokenTable[role])) -- non-strict filtering
  537.                  ) or
  538.                     (tokenTable[subgroup] and tokenTable[className]) -- strict filtering
  539.                 ) then
  540.                     tinsert(sortingTable, unit);
  541.                     sortingTable[unit] = name;
  542.                     if ( groupBy == "GROUP" ) then
  543.                         groupingTable[unit] = subgroup;
  544.  
  545.                     elseif ( groupBy == "CLASS" ) then
  546.                         groupingTable[unit] = className;
  547.  
  548.                     elseif ( groupBy == "ROLE" ) then
  549.                         groupingTable[unit] = role;
  550.  
  551.                     end
  552.                 end
  553.             end
  554.         end
  555.  
  556.         if ( groupBy ) then
  557.             local groupingOrder = self:GetAttribute("groupingOrder");
  558.             doubleFillTable(wipe(tokenTable), strsplit(",", groupingOrder));
  559.             if ( sortMethod == "NAME" ) then
  560.                 table.sort(sortingTable, sortOnGroupWithNames);
  561.             else
  562.                 table.sort(sortingTable, sortOnGroupWithIDs);
  563.             end
  564.         elseif ( sortMethod == "NAME" ) then -- sort by ID by default
  565.             table.sort(sortingTable, sortOnNames);
  566.  
  567.         end
  568.  
  569.     else
  570.         -- filtering via a list of names
  571.         doubleFillTable(tokenTable, strsplit(",", nameList));
  572.         for i = start, stop, 1 do
  573.             local unit, name = GetGroupRosterInfo(kind, i);
  574.             local petUnit = GetPetUnit(kind, i);
  575.             if ( filterOnPet ) then
  576.                 name = UnitName(petUnit);
  577.             end
  578.             if not ( useOwnerUnit ) then
  579.                 unit = petUnit;
  580.             end
  581.             if ( tokenTable[name] and UnitExists(petUnit) ) then
  582.                 tinsert(sortingTable, unit);
  583.                 sortingTable[unit] = name;
  584.             end
  585.         end
  586.         if ( sortMethod == "NAME" ) then
  587.             table.sort(sortingTable, sortOnNames);
  588.         end
  589.  
  590.     end
  591.  
  592.     configureChildren(self, sortingTable);
  593. end
  594.  
  595. -- SecureAuraHeader contributed by [email][email protected][/email]
  596.  
  597. --[[
  598. filter = [STRING] -- a pipe-separated list of aura filter options ("RAID" will be ignored)
  599. separateOwn = [NUMBER] -- indicate whether buffs you cast yourself should be separated before (1) or after (-1) others. If 0 or nil, no separation is done.
  600. sortMethod = ["INDEX", "NAME", "TIME"] -- defines how the group is sorted (Default: "INDEX")
  601. sortDirection = ["+", "-"] -- defines the sort order (Default: "+")
  602. groupBy = [nil, auraFilter] -- if present, a series of comma-separated filters, appended to the base filter to separate auras into groups within a single stream
  603. includeWeapons = [nil, NUMBER] -- The aura sub-stream before which to include temporary weapon enchants. If nil or 0, they are ignored.
  604. consolidateTo = [nil, NUMBER] -- The aura sub-stream before which to place a proxy for the consolidated header. If nil or 0, consolidation is ignored.
  605. consolidateDuration = [nil, NUMBER] -- the minimum total duration an aura should have to be considered for consolidation (Default: 30)
  606. consolidateThreshold = [nil, NUMBER] -- buffs with less remaining duration than this many seconds should not be consolidated (Default: 10)
  607. consolidateFraction = [nil, NUMBER] -- The fraction of remaining duration a buff should still have to be eligible for consolidation (Default: .10)
  608.  
  609. template = [STRING] -- the XML template to use for the unit buttons. If the created widgets should be something other than Buttons, append the Widget name after a comma.
  610. weaponTemplate = [STRING] -- the XML template to use for temporary enchant buttons. Can be nil if you preset the tempEnchant1 and tempEnchant2 attributes, or if you don't include temporary enchants.
  611. consolidateProxy = [STRING|Frame] -- Either the button which represents consolidated buffs, or the name of the template used to construct one.
  612. consolidateHeader = [STRING|Frame] -- Either the aura header which contains consolidated buffs, or the name of the template used to construct one.
  613.  
  614. point = [STRING] -- a valid XML anchoring point (Default: "TOPRIGHT")
  615. minWidth = [nil, NUMBER] -- the minimum width of the container frame
  616. minHeight = [nil, NUMBER] -- the minimum height of the container frame
  617. xOffset = [NUMBER] -- the x-Offset to use when anchoring the unit buttons. This should typically be set to at least the width of your buff template.
  618. yOffset = [NUMBER] -- the y-Offset to use when anchoring the unit buttons. This should typically be set to at least the height of your buff template.
  619. wrapAfter = [NUMBER] -- begin a new row or column after this many auras. If 0 or nil, never wrap or limit the first row
  620. wrapXOffset = [NUMBER] -- the x-offset from one row or column to the next
  621. wrapYOffset = [NUMBER] -- the y-offset from one row or column to the next
  622. maxWraps = [NUMBER] -- limit the number of rows or columns. If 0 or nil, the number of rows or columns will not be limited.
  623. --]]
  624.  
  625. local function SetupAuraButtonConfiguration( header, newChild, defaultConfigFunction )
  626.     local configCode = newChild:GetAttribute("initialConfigFunction") or header:GetAttribute("initialConfigFunction") or defaultConfigFunction;
  627.  
  628.     if ( type(configCode) == "string" ) then
  629.         local selfHandle = GetFrameHandle(newChild);
  630.         if ( selfHandle ) then
  631.             CallRestrictedClosure("self", GetManagedEnvironment(header, true),
  632.                                   selfHandle, configCode, selfHandle);
  633.         end
  634.     end
  635. end
  636.  
  637. function SecureAuraHeader_OnLoad(self)
  638.     self:RegisterEvent("UNIT_AURA");
  639. end
  640.  
  641. function SecureAuraHeader_OnUpdate(self)
  642.     local hasMainHandEnchant, hasOffHandEnchant, _;
  643.     hasMainHandEnchant, _, _, hasOffHandEnchant, _, _ = GetWeaponEnchantInfo();
  644.     if ( hasMainHandEnchant ~= self:GetAttribute("_mainEnchanted") ) then
  645.         self:SetAttribute("_mainEnchanted", hasMainHandEnchant);
  646.     end
  647.     if ( hasOffHandEnchant ~= self:GetAttribute("_secondaryEnchanted") ) then
  648.         self:SetAttribute("_secondaryEnchanted", hasOffHandEnchant);
  649.     end
  650. end
  651.  
  652. function SecureAuraHeader_OnEvent(self, event, ...)
  653.     if ( self:IsVisible() ) then
  654.         local unit = SecureButton_GetUnit(self);
  655.         if ( event == "UNIT_AURA" and ... == unit ) then
  656.             SecureAuraHeader_Update(self);
  657.         end
  658.     end
  659. end
  660.  
  661. function SecureAuraHeader_OnAttributeChanged(self, name, value)
  662.     if ( name == "_ignore" or self:GetAttribute("_ignore") ) then
  663.         return;
  664.     end
  665.     if ( self:IsVisible() ) then
  666.         SecureAuraHeader_Update(self);
  667.     end
  668. end
  669.  
  670. local buttons = {};
  671.  
  672. local function extractTemplateInfo(template, defaultWidget)
  673.     local widgetType;
  674.  
  675.     if ( template ) then
  676.         template, widgetType = strsplit(",", (tostring(template):trim():gsub("%s*,%s*", ",")) );
  677.         if ( template ~= "" ) then
  678.             if ( not widgetType or widgetType == "" ) then
  679.                 widgetType = defaultWidget;
  680.             end
  681.             return template, widgetType;
  682.         end
  683.     end
  684.     return nil;
  685. end
  686.  
  687. local function constructChild(kind, name, parent, template)
  688.     local new = CreateFrame(kind, name, parent, template);
  689.     SetupAuraButtonConfiguration(parent, new);
  690.     return new;
  691. end
  692.  
  693. local enchantableSlots = {
  694.     [1] = "MainHandSlot",
  695.     [2] = "SecondaryHandSlot",
  696.     [3] = "RangedSlot",
  697. }
  698.  
  699. local function configureAuras(self, auraTable, consolidateTable, weaponPosition)
  700.     local point = self:GetAttribute("point") or "TOPRIGHT";
  701.     local xOffset = tonumber(self:GetAttribute("xOffset")) or 0;
  702.     local yOffset = tonumber(self:GetAttribute("yOffset")) or 0;
  703.     local wrapXOffset = tonumber(self:GetAttribute("wrapXOffset")) or 0;
  704.     local wrapYOffset = tonumber(self:GetAttribute("wrapYOffset")) or 0;
  705.     local wrapAfter = tonumber(self:GetAttribute("wrapAfter"));
  706.     if ( wrapAfter == 0 ) then wrapAfter = nil; end
  707.     local maxWraps = self:GetAttribute("maxWraps");
  708.     if ( maxWraps == 0 ) then maxWraps = nil; end
  709.     local minWidth = tonumber(self:GetAttribute("minWidth")) or 0;
  710.     local minHeight = tonumber(self:GetAttribute("minHeight")) or 0;
  711.  
  712.     if ( consolidateTable and #consolidateTable == 0 ) then
  713.         consolidateTable = nil;
  714.     end
  715.     local name = self:GetName();
  716.  
  717.     wipe(buttons);
  718.     local buffTemplate, buffWidget = extractTemplateInfo(self:GetAttribute("template"), "Button");
  719.     if ( buffTemplate ) then
  720.         for i=1, #auraTable do
  721.             local childAttr = "child"..i;
  722.             local button = self:GetAttribute("child"..i);
  723.             if ( button ) then
  724.                 button:ClearAllPoints();
  725.             else
  726.                 button = constructChild(buffWidget, name and name.."AuraButton"..i, self, buffTemplate);
  727.                 setAttributesWithoutResponse(self, childAttr, button, "frameref-"..childAttr, GetFrameHandle(button));
  728.             end
  729.             local buffInfo = auraTable[i];
  730.             button:SetID(buffInfo.index);
  731.             button:SetAttribute("index", buffInfo.index);
  732.             button:SetAttribute("filter", buffInfo.filter);
  733.             buttons[i] = button;
  734.         end
  735.     end
  736.  
  737.     local consolidateProxy = self:GetAttribute("consolidateProxy");
  738.     if ( consolidateTable ) then
  739.         if ( type(consolidateProxy) == 'string' ) then
  740.             local template, widgetType = extractTemplateInfo(consolidateProxy, "Button");
  741.             if ( template ) then
  742.                 consolidateProxy = constructChild(widgetType, name and name.."ProxyButton", self, template);
  743.                 setAttributesWithoutResponse(self, "consolidateProxy", consolidateProxy, "frameref-proxy", GetFrameHandle(consolidateProxy));
  744.             else
  745.                 consolidateProxy = nil;
  746.             end
  747.         end
  748.         if ( consolidateProxy ) then
  749.             if ( consolidateTable.position ) then
  750.                 tinsert(buttons, consolidateTable.position, consolidateProxy);
  751.             else
  752.                 tinsert(buttons, consolidateProxy);
  753.             end
  754.             consolidateProxy:ClearAllPoints();
  755.         end
  756.     else
  757.         if ( consolidateProxy and type(consolidateProxy.Hide) == 'function' ) then
  758.             consolidateProxy:Hide();
  759.         end
  760.     end
  761.     if ( weaponPosition ) then
  762.         local hasMainHandEnchant, hasOffHandEnchant, hasRangedEnchant, _;
  763.         hasMainHandEnchant, _, _, hasOffHandEnchant, _, _, hasRangedEnchant, _, _ = GetWeaponEnchantInfo();
  764.  
  765.         for weapon=3,1,-1 do
  766.             local weaponAttr = "tempEnchant"..weapon
  767.             local tempEnchant = self:GetAttribute(weaponAttr)
  768.             if ( (select(weapon, hasMainHandEnchant, hasOffHandEnchant, hasRangedEnchant)) ) then
  769.                 if ( not tempEnchant ) then
  770.                     local template, widgetType = extractTemplateInfo(self:GetAttribute("weaponTemplate"), "Button");
  771.                     if ( template ) then
  772.                         tempEnchant = constructChild(widgetType, name and name.."TempEnchant"..weapon, self, template);
  773.                         setAttributesWithoutResponse(self, weaponAttr, tempEnchant);
  774.                     end
  775.                 end
  776.                 if ( tempEnchant ) then
  777.                     tempEnchant:ClearAllPoints();
  778.                     local slot = GetInventorySlotInfo(enchantableSlots[weapon]);
  779.                     tempEnchant:SetAttribute("target-slot", slot);
  780.                     tempEnchant:SetID(slot);
  781.                     if ( weaponPosition == 0 ) then
  782.                         tinsert(buttons, tempEnchant);
  783.                     else
  784.                         tinsert(buttons, weaponPosition, tempEnchant);
  785.                     end
  786.                 end
  787.             else
  788.                 if ( tempEnchant and type(tempEnchant.Hide) == 'function' ) then
  789.                     tempEnchant:Hide();
  790.                 end
  791.             end
  792.         end
  793.     end
  794.  
  795.     local display = #buttons
  796.     if ( wrapAfter and maxWraps ) then
  797.         display = min(display, wrapAfter * maxWraps);
  798.     end
  799.  
  800.     local left, right, top, bottom = math.huge, -math.huge, -math.huge, math.huge;
  801.     for index=1,display do
  802.         local button = buttons[index];
  803.         local wrapAfter = wrapAfter or index
  804.         local tick, cycle = floor((index - 1) % wrapAfter), floor((index - 1) / wrapAfter);
  805.         button:SetPoint(point, self, cycle * wrapXOffset + tick * xOffset, cycle * wrapYOffset + tick * yOffset);
  806.         button:Show();
  807.         left = min(left, button:GetLeft() or math.huge);
  808.         right = max(right, button:GetRight() or -math.huge);
  809.         top = max(top, button:GetTop() or -math.huge);
  810.         bottom = min(bottom, button:GetBottom() or math.huge);
  811.     end
  812.     local deadIndex = display + 1;
  813.     local button = self:GetAttribute("child"..deadIndex);
  814.     while ( button ) do
  815.         button:Hide();
  816.         deadIndex = deadIndex + 1;
  817.         button = self:GetAttribute("child"..deadIndex)
  818.     end
  819.    
  820.     if ( display >= 1 ) then
  821.         self:SetWidth(max(right - left, minWidth));
  822.         self:SetHeight(max(top - bottom, minHeight));
  823.     else
  824.         self:SetWidth(minWidth);
  825.         self:SetHeight(minHeight);
  826.     end
  827.     if ( consolidateTable ) then
  828.         local header = self:GetAttribute("consolidateHeader");
  829.         if ( type(header) == 'string' ) then
  830.             local template, widgetType = extractTemplateInfo(header, "Frame");
  831.             if ( template ) then
  832.                 header = constructChild(widgetType, name and name.."ProxyHeader", consolidateProxy, template);
  833.                 setAttributesWithoutResponse(self, "consolidateHeader", header);
  834.                 consolidateProxy:SetAttribute("header", header);
  835.                 consolidateProxy:SetAttribute("frameref-header", GetFrameHandle(header))
  836.             end
  837.         end
  838.         if ( header ) then
  839.             configureAuras(header, consolidateTable);
  840.         end
  841.     end
  842. end
  843.  
  844. local tremove = table.remove;
  845.  
  846. local function stripRAID(filter)
  847.     return filter and tostring(filter):upper():gsub("RAID", ""):gsub("|+", "|"):match("^|?(.+[^|])|?$");
  848. end
  849.  
  850. local freshTable;
  851. local releaseTable;
  852. do
  853.     local tableReserve = {};
  854.     freshTable = function ()
  855.         local t = next(tableReserve) or {};
  856.         tableReserve[t] = nil;
  857.         return t;
  858.     end
  859.     releaseTable = function (t)
  860.         tableReserve[t] = wipe(t);
  861.     end
  862. end
  863.  
  864. local sorters = {};
  865.  
  866. local function sortFactory(key, separateOwn, reverse)
  867.     if ( separateOwn ~= 0 ) then
  868.         if ( reverse ) then
  869.             return function (a, b)
  870.                 if ( groupingTable[a.filter] == groupingTable[b.filter] ) then
  871.                     local ownA, ownB = a.caster == "player", b.caster == "player";
  872.                     if ( ownA ~= ownB ) then
  873.                         return ownA == (separateOwn > 0)
  874.                     end
  875.                     return a[key] > b[key];
  876.                 else
  877.                     return groupingTable[a.filter] < groupingTable[b.filter];
  878.                 end
  879.             end;
  880.         else
  881.             return function (a, b)
  882.                 if ( groupingTable[a.filter] == groupingTable[b.filter] ) then
  883.                     local ownA, ownB = a.caster == "player", b.caster == "player";
  884.                     if ( ownA ~= ownB ) then
  885.                         return ownA == (separateOwn > 0)
  886.                     end
  887.                     return a[key] < b[key];
  888.                 else
  889.                     return groupingTable[a.filter] < groupingTable[b.filter];
  890.                 end
  891.             end;
  892.         end
  893.     else
  894.         if ( reverse ) then
  895.             return function (a, b)
  896.                 if ( groupingTable[a.filter] == groupingTable[b.filter] ) then
  897.                     return a[key] > b[key];
  898.                 else
  899.                     return groupingTable[a.filter] < groupingTable[b.filter];
  900.                 end
  901.             end;
  902.         else
  903.             return function (a, b)
  904.                 if ( groupingTable[a.filter] == groupingTable[b.filter] ) then
  905.                     return a[key] < b[key];
  906.                 else
  907.                     return groupingTable[a.filter] < groupingTable[b.filter];
  908.                 end
  909.             end;
  910.         end
  911.     end
  912. end
  913.  
  914. for i, key in ipairs{"index", "name", "expires"} do
  915.     local label = key:upper();
  916.     sorters[label] = {};
  917.     for bool in pairs{[true] = true, [false] = false} do
  918.         sorters[label][bool] = {}
  919.         for sep=-1,1 do
  920.             sorters[label][bool][sep] = sortFactory(key, sep, bool);
  921.         end
  922.     end
  923. end
  924. sorters.TIME = sorters.EXPIRES;
  925.  
  926. function SecureAuraHeader_Update(self)
  927.     local filter = self:GetAttribute("filter");
  928.     local groupBy = self:GetAttribute("groupBy");
  929.     local unit = SecureButton_GetUnit(self) or "player";
  930.     local includeWeapons = tonumber(self:GetAttribute("includeWeapons"));
  931.     if ( includeWeapons == 0 ) then
  932.         includeWeapons = nil
  933.     end
  934.     local consolidateTo = tonumber(self:GetAttribute("consolidateTo"));
  935.     local consolidateDuration, consolidateThreshold, consolidateFraction;
  936.     if ( consolidateTo ) then
  937.         consolidateDuration = tonumber(self:GetAttribute("consolidateDuration")) or 30;
  938.         consolidateThreshold = tonumber(self:GetAttribute("consolidateThreshold")) or 10;
  939.         consolidateFraction = tonumber(self:GetAttribute("consolidateFraction")) or 0.1;
  940.     end
  941.     local sortDirection = self:GetAttribute("sortDirection");
  942.     local separateOwn = tonumber(self:GetAttribute("separateOwn")) or 0;
  943.     if ( separateOwn > 0 ) then
  944.         separateOwn = 1;
  945.     elseif (separateOwn < 0 ) then
  946.         separateOwn = -1;
  947.     end
  948.     local sortMethod = (sorters[tostring(self:GetAttribute("sortMethod")):upper()] or sorters["INDEX"])[sortDirection == "-"][separateOwn];
  949.  
  950.     local time = GetTime();
  951.  
  952.     local consolidateTable;
  953.     if ( consolidateTo and consolidateTo ~= 0 ) then
  954.         consolidateTable = wipe(tokenTable);
  955.     end
  956.  
  957.     wipe(sortingTable);
  958.     wipe(groupingTable);
  959.  
  960.     if ( groupBy ) then
  961.         local i = 1;
  962.         for subFilter in groupBy:gmatch("[^,]+") do
  963.             if ( filter ) then
  964.                 subFilter = stripRAID(filter.."|"..subFilter);
  965.             else
  966.                 subFilter = stripRAID(subFilter);
  967.             end
  968.             groupingTable[subFilter], groupingTable[i] = i, subFilter;
  969.             i = i + 1;
  970.         end
  971.     else
  972.         filter = stripRAID(filter);
  973.         groupingTable[filter], groupingTable[1] = 1, filter;
  974.     end
  975.     if ( consolidateTable and consolidateTo < 0 ) then
  976.         consolidateTo = #groupingTable + consolidateTo + 1;
  977.     end
  978.     if ( includeWeapons and includeWeapons < 0 ) then
  979.         includeWeapons = #groupingTable + includeWeapons + 1;
  980.     end
  981.     local weaponPosition;
  982.     for filterIndex, fullFilter in ipairs(groupingTable) do
  983.         if ( consolidateTable and not consolidateTable.position and filterIndex >= consolidateTo ) then
  984.             consolidateTable.position = #sortingTable + 1;
  985.         end
  986.         if ( includeWeapons and not weaponPosition and filterIndex >= includeWeapons ) then
  987.             weaponPosition = #sortingTable + 1;
  988.         end
  989.  
  990.         local i = 1;
  991.         repeat
  992.             local aura, _, duration = freshTable();
  993.             aura.name, _, _, _, _, duration, aura.expires, aura.caster, _, aura.shouldConsolidate, _ = UnitAura(unit, i, fullFilter);
  994.             if ( aura.name ) then
  995.                 aura.filter = fullFilter;
  996.                 aura.index = i;
  997.                 local targetList = sortingTable;
  998.                 if ( consolidateTable and aura.shouldConsolidate ) then
  999.                     if ( not aura.expires or duration > consolidateDuration or (aura.expires - time >= max(consolidateThreshold, duration * consolidateFraction)) ) then
  1000.                         targetList = consolidateTable;
  1001.                     end
  1002.                 end
  1003.                 tinsert(targetList, aura);
  1004.             else
  1005.                 releaseTable(aura);
  1006.             end
  1007.             i = i + 1;
  1008.         until ( not aura.name );
  1009.     end
  1010.     if ( includeWeapons and not weaponPosition ) then
  1011.         weaponPosition = 0;
  1012.     end
  1013.     table.sort(sortingTable, sortMethod);
  1014.     if ( consolidateTable ) then
  1015.         table.sort(consolidateTable, sortMethod);
  1016.     end
  1017.  
  1018.     configureAuras(self, sortingTable, consolidateTable, weaponPosition);
  1019.     while ( sortingTable[1] ) do
  1020.         releaseTable(tremove(sortingTable));
  1021.     end
  1022.     while ( consolidateTable and consolidateTable[1] ) do
  1023.         releaseTable(tremove(consolidateTable));
  1024.     end
  1025. end


I have no idea what it would require to attempt to create something like this for oUF. I am not looking for an in-game GUI as I have no problems editing a nameList in a Lua file but, if someone would be willing to give this a shot, I (and I'm sure many others) would be grateful. I realize that people have regular lives and jobs outside of World of Warcraft and the add-ons that they maintain for it but... if someone did take this project on and this plug-in proved to be a monumental effort, I would be willing to compensate them for their time.

Thanks For Your Time,
-MiRai (ilillillil)

Last edited by MiRai : 03-03-12 at 12:07 PM. Reason: Title
  Reply With Quote
03-03-12, 12:43 PM   #2
p3lim
A Pyroguard Emberseer
 
p3lim's Avatar
AddOn Author - Click to view addons
Join Date: Feb 2007
Posts: 1,710
Code:
'sortMethod', 'NAMELIST',
'nameList', 'Bob,John,Kael',
  Reply With Quote
03-03-12, 02:28 PM   #3
MiRai
A Warpwood Thunder Caller
Join Date: Jul 2011
Posts: 96
Originally Posted by p3lim View Post
Code:
'sortMethod', 'NAMELIST',
'nameList', 'Bob,John,Kael',
I cannot believe it was just that easy. Thank you.
  Reply With Quote

WoWInterface » Featured Projects » oUF (Otravi Unit Frames) » oUFCustomLayouts


Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

vB code is On
Smilies are On
[IMG] code is On
HTML code is Off