diff --git a/api/api.lua b/api/api.lua new file mode 100644 index 0000000..74d4904 --- /dev/null +++ b/api/api.lua @@ -0,0 +1,1631 @@ +pfUI.api = { } + +-- load pfUI environment +setfenv(1, pfUI:GetEnvironment()) + +-- [ DLL Detection Helpers ] +-- Detects presence of various DLL extensions for enhanced functionality + +-- [ HasSuperWoW ] +-- Returns true if SuperWoW DLL is active +-- SuperWoW provides: UNIT_CASTEVENT, UnitPosition, SetMouseoverUnit, SpellInfo, etc. +function pfUI.api.HasSuperWoW() + return SUPERWOW_VERSION or (SetAutoloot and SpellInfo) +end + +-- [ HasUnitXP ] +-- Returns true if UnitXP_SP3 DLL is active +-- UnitXP provides: distance, line of sight, behind detection, targeting helpers +function pfUI.api.HasUnitXP() + local success = pcall(UnitXP, "nop", "nop") + return success +end + +-- [ HasNampower ] +-- Returns true if Nampower DLL is active +-- Nampower provides: spell queuing, GetCastInfo, GetSpellIdCooldown, IsSpellInRange, etc. +function pfUI.api.HasNampower() + return GetNampowerVersion and true or false +end + +-- [ GetUnitDistance ] +-- Returns distance to unit using best available method +-- 'unit1' [string] first unit (default: "player") +-- 'unit2' [string] second unit +-- returns: [number] distance in yards, or nil if unavailable +function pfUI.api.GetUnitDistance(unit1, unit2) + if not unit2 then + unit2 = unit1 + unit1 = "player" + end + + if not UnitExists(unit2) then return nil end + + -- Try UnitXP first (most accurate) + if pfUI.api.HasUnitXP() then + local success, distance = pcall(UnitXP, "distanceBetween", unit1, unit2) + if success and distance then return distance end + end + + -- Try SuperWoW UnitPosition + if pfUI.api.HasSuperWoW() and UnitPosition then + local x1, y1, z1 = UnitPosition(unit1) + local x2, y2, z2 = UnitPosition(unit2) + if x1 and y1 and z1 and x2 and y2 and z2 then + return ((x2 - x1)^2 + (y2 - y1)^2 + (z2 - z1)^2)^0.5 + end + end + + return nil +end + +-- [ UnitInLineOfSight ] +-- Returns true if unit1 has line of sight to unit2 +-- Requires UnitXP_SP3 +function pfUI.api.UnitInLineOfSight(unit1, unit2) + if not pfUI.api.HasUnitXP() then return nil end + local success, inSight = pcall(UnitXP, "inSight", unit1, unit2) + if success then return inSight end + return nil +end + +-- [ UnitIsBehind ] +-- Returns true if unit1 is behind unit2 +-- Requires UnitXP_SP3 +function pfUI.api.UnitIsBehind(unit1, unit2) + if not pfUI.api.HasUnitXP() then return nil end + local success, behind = pcall(UnitXP, "behind", unit1, unit2) + if success then return behind end + return nil +end + +-- Client API shortcuts +gfind = string.gmatch or string.gfind +mod = math.mod or mod + +-- [ strsplit ] +-- Splits a string using a delimiter. +-- 'delimiter' [string] characters that will be interpreted as delimiter +-- characters (bytes) in the string. +-- 'subject' [string] String to split. +-- return: [list] a list of strings. +function pfUI.api.strsplit(delimiter, subject) + if not subject then return nil end + local delimiter, fields = delimiter or ":", {} + local pattern = string.format("([^%s]+)", delimiter) + string.gsub(subject, pattern, function(c) fields[table.getn(fields)+1] = c end) + return unpack(fields) +end + +-- [ isempty ] +-- Returns true if a table is empty or not existing, otherwise false. +-- 'tbl' [table] the table that shall be checked +-- return: [boolean] result of the check. +function pfUI.api.isempty(tbl) + if not tbl then return true end + for k, v in pairs(tbl) do + return false + end + return true +end + +-- [ checkversion ] +-- Compares a given version (major,minor,fix) and compares it to the current +-- 'chkmajor' [number] the major number to check +-- 'chkminor' [number] the minor number to check +-- 'chkfix' [number] the fix number to check +-- +-- return: [boolean] true when the current version is smaller or equal +-- to the given value, otherwise returns nil. +local major, minor, fix = nil, nil, nil +function pfUI.api.checkversion(chkmajor, chkminor, chkfix) + if not major and not minor and not fix then + -- load and convert current version + major, minor, fix = pfUI.api.strsplit(".", tostring(pfUI_config.version)) + major, minor, fix = tonumber(major) or 0, tonumber(minor) or 0, tonumber(fix) or 0 + end + + local chkversion = chkmajor + chkminor/100 + chkfix/10000 + local curversion = major + minor/100 + fix/10000 + return curversion <= chkversion and true or nil +end + +-- [ UnitInRange ] +-- Returns whether a party/raid member is nearby. +-- It takes care of the rangecheck module if existing. +-- unit [string] A unit to query (string, unitID) +-- return: [bool] "1" if in range otherwise "nil" +local RangeCache = {} +function pfUI.api.UnitInRange(unit) + if not UnitExists(unit) or not UnitIsVisible(unit) then + return nil + elseif CheckInteractDistance(unit, 4) then + return 1 + end + + -- UnitXP precise mode: skip librange entirely, use direct distance check + if C.unitframes.rangecheck_mode == "unitxp" and _G.UnitXP then + local threshold = tonumber(C.unitframes.rangecheck_distance) or 40 + local success, distance = pcall(_G.UnitXP, "distanceBetween", "player", unit) + if success and distance then + return distance <= threshold and 1 or nil + end + -- fallthrough to librange if UnitXP fails + end + + if pfUI.api.librange then + return pfUI.api.librange:UnitInSpellRange(unit) + end + return nil +end + +-- [ RunOOC ] +-- Runs a function once, as soon as the combat lockdown fades. +-- func [function] The function that shall run ooc. +-- return: [bool] true if the function was added, +-- nil if the function already exists in queue +local queue, frame = {} +function pfUI.api.RunOOC(func) + if not frame then + frame = CreateFrame("Frame") + frame:SetScript("OnUpdate", function() + if InCombatLockdown and InCombatLockdown() then return end + for key, func in pairs(queue) do func(); queue[key] = nil end + end) + end + + if not queue[tostring(func)] then + queue[tostring(func)] = func + return true + end +end + +-- [ UnitHasBuff ] +-- Returns whether a unit has the given buff or not. +-- unit [string] A unit to query (string, unitID) +-- buff [string] The texture of the buff. +-- return: [bool] true if unit has buff otherwise "nil" +function pfUI.api.UnitHasBuff(unit, buff) + local hasbuff = nil + for i=1,32 do + if UnitBuff(unit, i) == buff then + hasbuff = true + break + end + end + + return hasbuff +end + +-- [[ GetUnitColor ]] +-- Returns an escape string for the unit aswell as the RGB values +-- unit [string] the unitstring +-- return: [table] string, r, g, b +function pfUI.api.GetUnitColor(unitstr) + local _, class = UnitClass(unitstr) + + local r, g, b = .8, .8, .8 + if RAID_CLASS_COLORS[class] then + r, g, b = RAID_CLASS_COLORS[class].r, RAID_CLASS_COLORS[class].g, RAID_CLASS_COLORS[class].b + end + + return pfUI.api.rgbhex(r,g,b), r, g, b +end + +-- [ strvertical ] +-- Creates vertical text using linebreaks. Multibyte char friendly. +-- 'str' [string] String to columnize. +-- return: [string] the string tranformed to a column. +function pfUI.api.strvertical(str) + local _, len = string.gsub(str,"[^\128-\193]", "") + if (len == string.len(str)) then + return string.gsub(str, "(.)", "%1\n") + else + return string.gsub(str,"([%z\1-\127\194-\244][\128-\191]*)", "%1\n") + end +end + +-- [ round ] +-- Rounds a float number into specified places after comma. +-- 'input' [float] the number that should be rounded. +-- 'places' [int] amount of places after the comma. +-- returns: [float] rounded number. +function pfUI.api.round(input, places) + if not places then places = 0 end + if type(input) == "number" and type(places) == "number" then + local pow = 1 + for i = 1, places do pow = pow * 10 end + return floor(input * pow + 0.5) / pow + end +end + +-- [ clamp ] +-- Clamps a number between given range. +-- 'x' [number] the number that should be clamped. +-- 'min' [number] minimum value. +-- 'max' [number] maximum value. +-- returns: [number] clamped value: 'x', 'min' or 'max' value itself. +function pfUI.api.clamp(x, min, max) + if type(x) == "number" and type(min) == "number" and type(max) == "number" then + return x < min and min or x > max and max or x + else + return x + end +end + +-- [ modf ] +-- Returns integral and fractional part of a number. +-- 'f' [float] the number to breakdown. +-- returns: [int],[float] whole and fractional part. +function pfUI.api.modf(f) + if modf then return modf(f) end + if f > 0 then + return math.floor(f), mod(f,1) + end + return math.ceil(f), mod(f,1) +end + +-- [ GetSlashCommands ] +-- Lists all registeres slash commands +-- 'text' [string] optional, a specific command to find +-- return: [list] a list of all matching slash commands +function pfUI.api.GetSlashCommands(text) + local cmds + for k, v in pairs(_G) do + if strfind(k, "^SLASH_") and (not text or v == text) then + cmds = cmds or {} + cmds[k] = v + end + end + + return cmds +end + +-- [ RegisterSlashCommand ] +-- Lists all registeres slash commands +-- 'name' [string] name of the command +-- 'cmds' [table] table containing all slash command strings +-- 'func' [function] the function that should be assigned +-- 'force' [boolean] force assign the command even if aleady provided +-- by another function/addon. +function pfUI.api.RegisterSlashCommand(name, cmds, func, force) + local counter = 1 + + for _, cmd in pairs(cmds) do + if force or not pfUI.api.GetSlashCommands(cmd) then + _G["SLASH_"..name..counter] = cmd + counter = counter + 1 + end + end + + _G.SlashCmdList[name] = func +end + +-- [ GetCaptures ] +-- Returns the indexes of a given regex pattern +-- 'pat' [string] unformatted pattern +-- returns: [numbers] capture indexes +local capture_cache = {} +function pfUI.api.GetCaptures(pat) + local r = capture_cache + + if not r[pat] then + for a, b, c, d, e in gfind(gsub(pat, "%((.+)%)", "%1"), gsub(pat, "%d%$", "%%(.-)$")) do + r[pat] = { a, b, c, d, e} + end + + r[pat] = r[pat] or {} + end + + return r[pat][1], r[pat][2], r[pat][3], r[pat][4], r[pat][5] +end + +-- [ SanitizePattern ] +-- Sanitizes and convert patterns into gfind compatible ones. +-- 'pattern' [string] unformatted pattern +-- returns: [string] simplified gfind compatible pattern +local sanitize_cache = {} +function pfUI.api.SanitizePattern(pattern) + if not sanitize_cache[pattern] then + local ret = pattern + -- escape magic characters + ret = gsub(ret, "([%+%-%*%(%)%?%[%]%^])", "%%%1") + -- remove capture indexes + ret = gsub(ret, "%d%$","") + -- catch all characters + ret = gsub(ret, "(%%%a)","%(%1+%)") + -- convert all %s to .+ + ret = gsub(ret, "%%s%+",".+") + -- set priority to numbers over strings + ret = gsub(ret, "%(.%+%)%(%%d%+%)","%(.-%)%(%%d%+%)") + -- cache it + sanitize_cache[pattern] = ret + end + + return sanitize_cache[pattern] +end + +-- [ cmatch ] +-- Same as string.match but aware of capture indexes (up to 5) +-- 'str' [string] input string that should be matched +-- 'pat' [string] unformatted pattern +-- returns: [strings] matched string in capture order +local a, b, c, d, e +local _, va, vb, vc, vd, ve +local ra, rb, rc, rd, re +function pfUI.api.cmatch(str, pat) + -- read capture indexes + a, b, c, d, e = GetCaptures(pat) + _, _, va, vb, vc, vd, ve = string.find(str, pfUI.api.SanitizePattern(pat)) + + -- put entries into the proper return values + ra = e == 1 and ve or d == 1 and vd or c == 1 and vc or b == 1 and vb or va + rb = e == 2 and ve or d == 2 and vd or c == 2 and vc or a == 2 and va or vb + rc = e == 3 and ve or d == 3 and vd or a == 3 and va or b == 3 and vb or vc + rd = e == 4 and ve or a == 4 and va or c == 4 and vc or b == 4 and vb or vd + re = a == 5 and va or d == 5 and vd or c == 5 and vc or b == 5 and vb or ve + + return ra, rb, rc, rd, re +end + +-- [ GetItemLinkByName ] +-- Returns an itemLink for the given itemname +-- 'name' [string] name of the item +-- returns: [string] entire itemLink for the given item +function pfUI.api.GetItemLinkByName(name) + for itemID = 1, 25818 do + local itemName, hyperLink, itemQuality = GetItemInfo(itemID) + if (itemName and itemName == name) then + local _, _, _, hex = GetItemQualityColor(tonumber(itemQuality)) + return hex.. "|H"..hyperLink.."|h["..itemName.."]|h|r" + end + end +end + +-- [ GetItemCount ] +-- Returns information about how many of a given item the player has. +-- 'itemName' [string] name of the item +-- returns: [int] the number of the given item +function pfUI.api.GetItemCount(itemName) + local count = 0 + for bag = 4, 0, -1 do + for slot = 1, GetContainerNumSlots(bag) do + local _, itemCount = GetContainerItemInfo(bag, slot) + if itemCount then + local itemLink = GetContainerItemLink(bag,slot) + local _, _, itemParse = strfind(itemLink, "(%d+):") + local queryName = GetItemInfo(itemParse) + if queryName and queryName ~= "" then + if queryName == itemName then + count = count + itemCount + end + end + end + end + end + + return count +end + +-- [ FindItem ] +-- Returns the bag and slot position of an item based on the name. +-- 'item' [string] name of the item +-- returns: [int] bag +-- [int] slot +function pfUI.api.FindItem(item) + for bag = 4, 0, -1 do + for slot = 1, GetContainerNumSlots(bag) do + local itemLink = GetContainerItemLink(bag,slot) + if itemLink then + local _, _, parse = strfind(itemLink, "(%d+):") + local query = GetItemInfo(parse) + if query and query ~= "" and string.lower(query) == string.lower(item) then + return bag, slot + end + end + end + end + + return nil +end + +-- [ GetBagFamily ] +-- Returns information about the type of a bag such as Soul Bags or Quivers. +-- Available bagtypes are "BAG", "KEYRING", "SOULBAG", "QUIVER" and "SPECIAL" +-- 'bag' [int] the bag id +-- returns: [string] the type of the bag, e.g "QUIVER" +function pfUI.api.GetBagFamily(bag) + if bag == -2 then return "KEYRING" end + if bag == 0 then return "BAG" end -- backpack + if bag == -1 then return "BAG" end -- bank + + local _, _, id = strfind(GetInventoryItemLink("player", ContainerIDToInventoryID(bag)) or "", "item:(%d+)") + if id then + local _, _, _, _, _, itemType, subType = GetItemInfo(id) + local bagsubtype = L["bagtypes"][subType] + + if bagsubtype == "DEFAULT" then return "BAG" end + if bagsubtype == "SOULBAG" then return "SOULBAG" end + if bagsubtype == "QUIVER" then return "QUIVER" end + if bagsubtype == nil then return "SPECIAL" end + end + + return nil +end + +-- [ Abbreviate ] +-- Abbreviates a number from 1234 to 1.23k +-- 'number' [number] the number that should be abbreviated +-- 'returns: [string] the abbreviated value +function pfUI.api.Abbreviate(number) + local mode = pfUI_config.unitframes.abbrevnum + -- mode "0" = disabled (full numbers) + -- mode "1" = 2 decimals (4250 -> 4.25k) [legacy/default] + -- mode "2" = 1 decimal (4250 -> 4.2k) - always rounds DOWN + + if mode == "1" or mode == "2" then + local sign = number < 0 and -1 or 1 + number = math.abs(number) + + if number > 1000000 then + if mode == "2" then + -- 1 decimal, round DOWN: 4.18m -> 4.1m + return (floor(number/100000) / 10 * sign) .. "m" + else + return pfUI.api.round(number/1000000*sign, 2) .. "m" + end + elseif number > 1000 then + if mode == "2" then + -- 1 decimal, round DOWN: 4180 -> 4.1k (not 4.2k) + return (floor(number/100) / 10 * sign) .. "k" + else + return pfUI.api.round(number/1000*sign, 2) .. "k" + end + end + end + + return math.floor(number) +end + +-- [ SendChatMessageWide ] +-- Sends a message to widest audience the player can broadcast to +-- 'msg' [string] the message to send +function pfUI.api.SendChatMessageWide(msg) + local channel = "SAY" + if UnitInRaid("player") then + if ( IsRaidLeader() or IsRaidOfficer() ) then + channel = "RAID_WARNING" + else + channel = "RAID" + end + elseif UnitExists("party1") then + channel = "PARTY" + end + SendChatMessage(msg,channel) +end + +-- [ GroupInfoByName ] +-- Gets the unit id by name +-- 'name' [string] party or raid member +-- 'group' [string] "raid" or "party" +-- returns: [table] {name='name',unitId='unitId',Id=Id,lclass='lclass',class='class'} +do -- create a scope so we don't have to worry about upvalue collisions + local party, raid, unitinfo = {}, {}, {} + party[0] = "player" -- fake unit + for i=1, MAX_PARTY_MEMBERS do + party[i] = "party"..i + end + for i=1, MAX_RAID_MEMBERS do + raid[i] = "raid"..i + end + function pfUI.api.GroupInfoByName(name,group) + unitinfo = pfUI.api.wipe(unitinfo) + if group == "party" then + for i=0, MAX_PARTY_MEMBERS do + local unitName = UnitName(party[i]) + if unitName == name then + local lclass,class = UnitClass(party[i]) + if not (lclass and class) then + lclass,class = _G.UNKNOWN, "UNKNOWN" + end + unitinfo.name,unitinfo.unitId,unitinfo.Id,unitinfo.lclass,unitinfo.class = + unitName,party[i],i,lclass,class + return unitinfo + end + end + elseif group == "raid" then + for i=1, MAX_RAID_MEMBERS do + local unitName = UnitName(raid[i]) + if unitName == name then + local lclass,class = UnitClass(raid[i]) + if not (lclass and class) then + lclass,class = _G.UNKNOWN, "UNKNOWN" + end + unitinfo.name,unitinfo.unitId,unitinfo.Id,unitinfo.lclass,unitinfo.class = + unitName,raid[i],i,lclass,class + return unitinfo + end + end + end + -- fallback for GetMasterLootCandidate not updating immediately for leavers + unitinfo.lclass,unitinfo.class = _G.UNKNOWN, "UNKNOWN" + return unitinfo + end +end + +-- [ HookScript ] +-- Securely post-hooks a script handler. +-- 'f' [frame] the frame which needs a hook +-- 'script' [string] the handler to hook +-- 'func' [function] the function that should be added +function HookScript(f, script, func) + local prev = f:GetScript(script) + f:SetScript(script, function(a1,a2,a3,a4,a5,a6,a7,a8,a9) + if prev then prev(a1,a2,a3,a4,a5,a6,a7,a8,a9) end + func(a1,a2,a3,a4,a5,a6,a7,a8,a9) + end) +end + +-- [ HookAddonOrVariable ] +-- Sets a function to be called automatically once an addon gets loaded +-- 'addon' [string] addon or variable name +-- 'func' [function] function that should run +function pfUI.api.HookAddonOrVariable(addon, func) + local lurker = CreateFrame("Frame", nil) + lurker.func = func + lurker:RegisterEvent("ADDON_LOADED") + lurker:RegisterEvent("VARIABLES_LOADED") + lurker:RegisterEvent("PLAYER_ENTERING_WORLD") + lurker:SetScript("OnEvent",function() + -- only run when config is available + if event == "ADDON_LOADED" and not this.foundConfig then + return + elseif event == "VARIABLES_LOADED" then + this.foundConfig = true + end + + if IsAddOnLoaded(addon) or _G[addon] then + this:func() + this:UnregisterAllEvents() + end + end) +end + +-- [ QueueFunction ] +-- Add functions to a FIFO queue for execution after a short delay. +-- '...' [vararg] function, [arguments] +local timer +function pfUI.api.QueueFunction(a1,a2,a3,a4,a5,a6,a7,a8,a9) + if not timer then + timer = CreateFrame("Frame") + timer.queue = {} + timer.interval = TOOLTIP_UPDATE_TIME + timer.DeQueue = function() + local item = table.remove(timer.queue,1) + if item then + item[1](item[2],item[3],item[4],item[5],item[6],item[7],item[8],item[9]) + end + if table.getn(timer.queue) == 0 then + timer:Hide() -- no need to run the OnUpdate when the queue is empty + end + end + timer:SetScript("OnUpdate",function() + this.sinceLast = (this.sinceLast or 0) + arg1 + while (this.sinceLast > this.interval) do + this.DeQueue() + this.sinceLast = this.sinceLast - this.interval + end + end) + end + table.insert(timer.queue,{a1,a2,a3,a4,a5,a6,a7,a8,a9}) + timer:Show() -- start the OnUpdate +end + +-- [ Create Gold String ] +-- Transforms a amount of copper into a fully fledged gold string +-- 'money' [int] the amount of coppy (GetMoney()) +-- return: [string] a colorized string which is split into +-- gold,silver and copper values. +function pfUI.api.CreateGoldString(money) + if type(money) ~= "number" then return "-" end + + local gold = floor(money/ 100 / 100) + local silver = floor(mod((money/100),100)) + local copper = floor(mod(money,100)) + + local string = "" + if gold > 0 then string = string .. "|cffffffff" .. gold .. "|cffffd700g" end + if silver > 0 or gold > 0 then string = string .. "|cffffffff " .. silver .. "|cffc7c7cfs" end + string = string .. "|cffffffff " .. copper .. "|cffeda55fc" + + return string +end + +-- [ Enable Movable ] +-- Set all necessary functions to make a already existing frame movable. +-- 'name' [frame/string] Name of the Frame that should be movable +-- 'addon' [string] Addon that must be loaded before being able to access the frame +-- 'blacklist' [table] A list of frames that should be deactivated for mouse usage +local function OnDragStart() this:StartMoving() end +local function OnDragStop() this:StopMovingOrSizing() end +function pfUI.api.EnableMovable(name, addon, blacklist) + if addon then + local scan = CreateFrame("Frame") + scan:RegisterEvent("ADDON_LOADED") + scan:SetScript("OnEvent", function() + if arg1 == addon then + local frame = _G[name] + + if blacklist then + for _, disable in pairs(blacklist) do + _G[disable]:EnableMouse(false) + end + end + + frame:SetMovable(true) + frame:EnableMouse(true) + frame:RegisterForDrag("LeftButton") + frame:SetScript("OnDragStart", OnDragStart) + frame:SetScript("OnDragStop", OnDragStop) + + this:UnregisterAllEvents() + end + end) + else + if blacklist then + for _, disable in pairs(blacklist) do + _G[disable]:EnableMouse(false) + end + end + + local frame = name + if type(name) == "string" then frame = _G[name] end + frame:SetMovable(true) + frame:EnableMouse(true) + frame:RegisterForDrag("LeftButton") + frame:SetScript("OnDragStart", OnDragStart) + frame:SetScript("OnDragStop", OnDragStop) + end +end + +-- [ Copy Table ] +-- By default a table assignment only will be a reference instead of a copy. +-- This is used to create a replicate of the actual table. +-- 'src' [table] the table that should be copied. +-- return: [table] the replicated table. +function pfUI.api.CopyTable(src) + local lookup_table = {} + local function _copy(src) + if type(src) ~= "table" then + return src + elseif lookup_table[src] then + return lookup_table[src] + end + local new_table = {} + lookup_table[src] = new_table + for index, value in pairs(src) do + new_table[_copy(index)] = _copy(value) + end + return setmetatable(new_table, getmetatable(src)) + end + return _copy(src) +end + +-- [ Wipe Table ] +-- Empties a table and returns it +-- 'src' [table] the table that should be emptied. +-- return: [table] the emptied table. +function pfUI.api.wipe(src) + -- notes: table.insert, table.remove will have undefined behavior + -- when used on tables emptied this way because Lua removes nil + -- entries from tables after an indeterminate time. + -- Instead of table.insert(t,v) use t[table.getn(t)+1]=v as table.getn collapses nil entries. + -- There are no issues with hash tables, t[k]=v where k is not a number behaves as expected. + local mt = getmetatable(src) or {} + if mt.__mode == nil or mt.__mode ~= "kv" then + mt.__mode = "kv" + src=setmetatable(src,mt) + end + for k in pairs(src) do + src[k] = nil + end + return src +end + +-- [ Load Movable ] +-- Loads the positions of a Frame. +-- 'frame' [frame] the frame that should be positioned. +-- 'init' [bool] treats the current position as initial data +function pfUI.api.LoadMovable(frame, init) + -- update position data + if not frame.posdata or init then + frame.posdata = { scale = frame:GetScale(), pos = {} } + for i=1,frame:GetNumPoints() do + frame.posdata.pos[i] = { frame:GetPoint(i) } + end + end + + if pfUI_config["position"][frame:GetName()] then + if pfUI_config["position"][frame:GetName()]["parent"] then + frame:SetParent(_G[pfUI_config["position"][frame:GetName()]["parent"]]) + end + + if pfUI_config["position"][frame:GetName()]["scale"] then + frame:SetScale(pfUI_config["position"][frame:GetName()].scale) + end + + if pfUI_config["position"][frame:GetName()]["xpos"] then + local anchor = pfUI_config["position"][frame:GetName()]["anchor"] or "TOPLEFT" + frame:ClearAllPoints() + frame:SetPoint(anchor, pfUI_config["position"][frame:GetName()].xpos, pfUI_config["position"][frame:GetName()].ypos) + end + elseif frame.posdata and frame.posdata.pos[1] then + frame:ClearAllPoints() + frame:SetScale(frame.posdata.scale) + + for id, point in pairs(frame.posdata.pos) do + local a, b, c, d, e = unpack(point) + -- skip if anchoring to itself (would cause WoW error) + if a and b and b ~= frame and b ~= frame:GetName() then + frame:SetPoint(a,b,c,d,e) + elseif a and (not b or b == frame or b == frame:GetName()) then + frame:SetPoint(a,d,e) + end + end + end +end + +-- [ Save Movable ] +-- Save the positions of a Frame. +-- 'frame' [frame] the frame that should be saved. +function pfUI.api.SaveMovable(frame, scale) + local anchor, _, _, xpos, ypos = frame:GetPoint() + C.position[frame:GetName()] = C.position[frame:GetName()] or {} + C.position[frame:GetName()]["xpos"] = round(xpos) + C.position[frame:GetName()]["ypos"] = round(ypos) + C.position[frame:GetName()]["anchor"] = anchor + C.position[frame:GetName()]["parent"] = frame:GetParent() and frame:GetParent():GetName() or nil + if scale then + C.position[frame:GetName()]["scale"] = frame:GetScale() + end +end + +-- [ Update Movable ] +-- Loads and update the configured position of the specified frame. +-- It also creates an entry in the movables table. +-- 'frame' [frame] the frame that should be updated. +-- 'init' [bool] treats the current position as initial data +function pfUI.api.UpdateMovable(frame, init) + local name = frame:GetName() + + if pfUI_config.global.offscreen == "0" then + frame:SetClampedToScreen(true) + end + + if not pfUI.movables[name] then + pfUI.movables[name] = frame + end + + LoadMovable(frame, init) +end + +-- [ Remove Movable ] +-- Removes a Frame from the movable list. +-- 'frame' [frame] the frame that should be removed. +function pfUI.api.RemoveMovable(frame) + local name = frame:GetName() + pfUI.movables[name] = nil +end + +-- [ AlignToPosition ] +-- Sets a frame to the selected anchor +-- 'frame' [frame] the frame that should be aligned +-- 'anchor' [frame] the frame where it should be aligned to +-- 'position' [string] where it should appear, takes the following: +-- "TOP", "RIGHT", "BOTTOM", "LEFT" +function pfUI.api.AlignToPosition(frame, anchor, position, spacing) + if frame == anchor then return end + frame:ClearAllPoints() + if position == "TOP" and anchor then + frame:SetPoint("BOTTOMLEFT", anchor, "TOPLEFT", 0, (spacing or 0)) + frame:SetPoint("BOTTOMRIGHT", anchor, "TOPRIGHT", 0, (spacing or 0)) + elseif position == "RIGHT" and anchor then + frame:SetPoint("TOPLEFT", anchor, "TOPRIGHT", (spacing or 0), 0) + frame:SetPoint("BOTTOMLEFT", anchor, "BOTTOMRIGHT", (spacing or 0), 0) + elseif position == "BOTTOM" and anchor then + frame:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -(spacing or 0)) + frame:SetPoint("TOPRIGHT", anchor, "BOTTOMRIGHT", 0, -(spacing or 0)) + elseif position == "LEFT" and anchor then + frame:SetPoint("TOPRIGHT", anchor, "TOPLEFT", -(spacing or 0), 0) + frame:SetPoint("BOTTOMRIGHT", anchor, "BOTTOMLEFT", -(spacing or 0), 0) + else + frame:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", 0, 0) + frame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", 0, 0) + end +end + +-- [ SetAutoPoint ] +-- Automatically places the frame according to screen position of the parent. +-- 'frame' [frame] the frame that should be moved. +-- 'parent' [frame] the frame's anchor point +-- 'spacing' [number] the padding that should be used between the +-- frame and its parent frame +function pfUI.api.SetAutoPoint(frame, parent, spacing) + --[[ + + a b max + +-----------------+ + | 1 | 2 | 3 | + |-----+-----+-----| c + | 4 | 5 | 6 | + |-----+-----+-----| d + | 7 | 8 | 9 | + +-----------------+ + 0 + + ]]-- + + local a = GetScreenWidth() / 3 + local b = GetScreenWidth() / 3 * 2 + + local c = GetScreenHeight() / 3 * 2 + local d = GetScreenHeight() / 3 + + local x, y = parent:GetCenter() + + if not x or not y then return end + + local off = spacing or 0 + + frame:ClearAllPoints() + + if x < a and y > c then + -- TOPLEFT + frame:SetPoint("TOPLEFT", parent, "BOTTOMLEFT", 0, -off) + elseif x > a and x < b and y > c then + -- TOP + frame:SetPoint("TOP", parent, "BOTTOM", 0, -off) + elseif x > b and y > c then + -- TOPRIGHT + frame:SetPoint("TOPRIGHT", parent, "BOTTOMRIGHT", 0, -off) + + elseif x < a and y > d and y < c then + -- LEFT + frame:SetPoint("LEFT", parent, "RIGHT", off, 0) + + elseif x > a and x < b and y > d and y < c then + -- CENTER + frame:SetPoint("BOTTOM", parent, "TOP", 0, off) + + elseif x > b and y > d and y < c then + -- RIGHT + frame:SetPoint("RIGHT", parent, "LEFT", -off, 0) + + elseif x < a and y < d then + -- BOTTOMLEFT + frame:SetPoint("BOTTOMLEFT", parent, "TOPLEFT", 0, off) + + elseif x > a and x < b and y < d then + -- BOTTOM + frame:SetPoint("BOTTOM", parent, "TOP", 0, off) + + elseif x > b and y < d then + -- BOTTOMRIGHT + frame:SetPoint("BOTTOMRIGHT", parent, "TOPRIGHT", 0, off) + end +end + +-- [ GetBestAnchor ] +-- Returns the best anchor of a frame, based on its position +-- 'self' [frame] the frame that should be checked +-- returns: [string] the name of the best anchor +function pfUI.api.GetBestAnchor(self) + local scale = self:GetScale() + local x, y = self:GetCenter() + local a = GetScreenWidth() / scale / 3 + local b = GetScreenWidth() / scale / 3 * 2 + local c = GetScreenHeight() / scale / 3 * 2 + local d = GetScreenHeight() / scale / 3 + if not x or not y then return end + + if x < a and y > c then + return "TOPLEFT" + elseif x > a and x < b and y > c then + return "TOP" + elseif x > b and y > c then + return "TOPRIGHT" + elseif x < a and y > d and y < c then + return "LEFT" + elseif x > a and x < b and y > d and y < c then + return "CENTER" + elseif x > b and y > d and y < c then + return "RIGHT" + elseif x < a and y < d then + return "BOTTOMLEFT" + elseif x > a and x < b and y < d then + return "BOTTOM" + elseif x > b and y < d then + return "BOTTOMRIGHT" + end +end + +-- [ ConvertFrameAnchor ] +-- Converts a frame anchor into another one while preserving the frame position +-- 'self' [frame] the frame that should get another anchor. +-- 'anchor' [string] the new anchor that shall be used +-- returns: anchor, x, y can directly be used in SetPoint() +function pfUI.api.ConvertFrameAnchor(self, anchor) + local scale, x, y, _ = self:GetScale(), nil, nil, nil + + if anchor == "CENTER" then + x, y = self:GetCenter() + x, y = x - GetScreenWidth()/2/scale, y - GetScreenHeight()/2/scale + elseif anchor == "TOPLEFT" then + x, y = self:GetLeft(), self:GetTop() - GetScreenHeight()/scale + elseif anchor == "TOP" then + x, _ = self:GetCenter() + x, y = x - GetScreenWidth()/2/scale, self:GetTop() - GetScreenHeight()/scale + elseif anchor == "TOPRIGHT" then + x, y = self:GetRight() - GetScreenWidth()/scale, self:GetTop() - GetScreenHeight()/scale + elseif anchor == "RIGHT" then + _, y = self:GetCenter() + x, y = self:GetRight() - GetScreenWidth()/scale, y - GetScreenHeight()/2/scale + elseif anchor == "BOTTOMRIGHT" then + x, y = self:GetRight() - GetScreenWidth()/scale, self:GetBottom() + elseif anchor == "BOTTOM" then + x, _ = self:GetCenter() + x, y = x - GetScreenWidth()/2/scale, self:GetBottom() + elseif anchor == "BOTTOMLEFT" then + x, y = self:GetLeft(), self:GetBottom() + elseif anchor == "LEFT" then + _, y = self:GetCenter() + x, y = self:GetLeft(), y - GetScreenHeight()/2/scale + end + + return anchor, round(x, 2), round(y, 2) +end + +-- [ GetStringColor ] +-- Queries the pfUI setting strings and extract its color codes +-- returns r,g,b,a +local color_cache = {} +function pfUI.api.GetStringColor(colorstr) + if not color_cache[colorstr] then + local r, g, b, a = pfUI.api.strsplit(",", colorstr) + color_cache[colorstr] = { r, g, b, a } + end + return unpack(color_cache[colorstr]) +end + +-- [ rgbhex ] +-- Returns color format from color info +-- 'r' [table | number] color table or r color component +-- 'g' [number] optional g color component +-- 'b' [number] optional b color component +-- 'a' [number] optional alpha component +-- returns color string in the form of '|caarrggbb' +local _r, _g, _b, _a +function pfUI.api.rgbhex(r, g, b, a) + if type(r) == "table" then + if r.r then + _r, _g, _b, _a = r.r, r.g, r.b, (r.a or 1) + elseif table.getn(r) >= 3 then + _r, _g, _b, _a = r[1], r[2], r[3], (r[4] or 1) + end + elseif tonumber(r) then + _r, _g, _b, _a = r, g, b, (a or 1) + end + + if _r and _g and _b and _a then + -- limit values to 0-1 + _r = _r + 0 > 1 and 1 or _r + 0 + _g = _g + 0 > 1 and 1 or _g + 0 + _b = _b + 0 > 1 and 1 or _b + 0 + _a = _a + 0 > 1 and 1 or _a + 0 + return string.format("|c%02x%02x%02x%02x", _a*255, _r*255, _g*255, _b*255) + end + + return "" +end + +-- [ GetBorderSize ] +-- Returns the configure value of a border and its pixel scaled version. +-- 'pref' allows to specifiy a custom border (i.e unitframes, panel) +function pfUI.api.GetBorderSize(pref) + if not pfUI.borders then pfUI.borders = {} end + + -- set to default border if accessing a wrong border type + if not pref or not pfUI_config.appearance.border[pref] or pfUI_config.appearance.border[pref] == "-1" then + pref = "default" + end + + if pfUI.borders[pref] then + -- return already cached values + return pfUI.borders[pref][1], pfUI.borders[pref][2] + else + -- add new borders to the pfUI tree + local raw = tonumber(pfUI_config.appearance.border[pref]) + if raw == -1 then raw = 3 end + + local scaled = raw * GetPerfectPixel() + pfUI.borders[pref] = { raw, scaled } + + return raw, scaled + end +end + +-- [ GetPerfectPixel ] +-- Returns a number that equals a real pixel on regular scaled frames. +-- Respects the current UI-scale and calculates a real pixel based on +-- the screen resolution and the 768px sized drawlayer. +function pfUI.api.GetPerfectPixel() + if pfUI.pixel then return pfUI.pixel end + + if pfUI_config.appearance.border.pixelperfect == "1" then + local scale = GetCVar("useUiScale") == "1" and GetCVar("uiScale") or "1" + local resolution = GetCVar("gxResolution") + local _, _, screenwidth, screenheight = strfind(resolution, "(.+)x(.+)") + + pfUI.pixel = 768 / screenheight / scale + pfUI.pixel = pfUI.pixel > 1 and 1 or pfUI.pixel + + -- autodetect and zoom for HiDPI displays + if pfUI_config.appearance.border.hidpi == "1" then + pfUI.pixel = pfUI.pixel < .5 and pfUI.pixel * 2 or pfUI.pixel + end + else + pfUI.pixel = .7 + end + + pfUI.backdrop = { + bgFile = "Interface\\BUTTONS\\WHITE8X8", tile = false, tileSize = 0, + edgeFile = "Interface\\BUTTONS\\WHITE8X8", edgeSize = pfUI.pixel, + insets = {left = -pfUI.pixel, right = -pfUI.pixel, top = -pfUI.pixel, bottom = -pfUI.pixel}, + } + + pfUI.backdrop_thin = { + bgFile = "Interface\\BUTTONS\\WHITE8X8", tile = false, tileSize = 0, + edgeFile = "Interface\\BUTTONS\\WHITE8X8", edgeSize = pfUI.pixel, + insets = {left = 0, right = 0, top = 0, bottom = 0}, + } + + return pfUI.pixel +end + +-- [ Create Backdrop ] +-- Creates a pfUI compatible frame as backdrop element +-- 'f' [frame] the frame which should get a backdrop. +-- 'inset' [int] backdrop inset, defaults to border size. +-- 'legacy' [bool] use legacy backdrop instead of creating frames. +-- 'transp' [number] set default transparency +local backdrop, b, level, rawborder, border, br, bg, bb, ba, er, eg, eb, ea +function pfUI.api.CreateBackdrop(f, inset, legacy, transp, backdropSetting) + -- exit if now frame was given + if not f then return end + + -- load raw and pixel perfect scaled border + rawborder, border = GetBorderSize() + + -- load custom border if existing + if inset then + rawborder = inset / GetPerfectPixel() + border = inset + end + + -- detect if blizzard backdrops shall be used + local blizz = C.appearance.border.force_blizz == "1" and true or nil + backdrop = blizz and pfUI.backdrop_blizz_full or rawborder == 1 and pfUI.backdrop_thin or pfUI.backdrop + border = blizz and math.max(border, 3) or border + + -- get the color settings + br, bg, bb, ba = pfUI.api.GetStringColor(pfUI_config.appearance.border.background) + er, eg, eb, ea = pfUI.api.GetStringColor(pfUI_config.appearance.border.color) + + if transp and transp < tonumber(ba) then ba = transp end + + -- use legacy backdrop handling + if legacy then + if backdropSetting then f:SetBackdrop(backdropSetting) end + f:SetBackdrop(backdrop) + f:SetBackdropColor(br, bg, bb, ba) + f:SetBackdropBorderColor(er, eg, eb , ea) + else + -- increase clickable area if available + if f.SetHitRectInsets and ( not InCombatLockdown or not InCombatLockdown()) then + f:SetHitRectInsets(-border,-border,-border,-border) + end + + -- use new backdrop behaviour + if not f.backdrop then + if f:GetBackdrop() then f:SetBackdrop(nil) end + + local b = CreateFrame("Frame", nil, f) + level = f:GetFrameLevel() + if level < 1 then + b:SetFrameLevel(level) + else + b:SetFrameLevel(level - 1) + end + + f.backdrop = b + end + + f.backdrop:SetPoint("TOPLEFT", f, "TOPLEFT", -border, border) + f.backdrop:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", border, -border) + f.backdrop:SetBackdrop(backdrop) + f.backdrop:SetBackdropColor(br, bg, bb, ba) + f.backdrop:SetBackdropBorderColor(er, eg, eb , ea) + + if blizz then + if not f.backdrop_border then + local border = CreateFrame("Frame", nil, f.backdrop) + border:SetFrameLevel(level + 2) + f.backdrop_border = border + + local hookSetBackdropBorderColor = f.backdrop.SetBackdropBorderColor + f.backdrop.SetBackdropBorderColor = function(self, r, g, b, a) + f.backdrop_border:SetBackdropBorderColor(r, g, b, a) + hookSetBackdropBorderColor(f.backdrop, r, g, b, a) + end + end + + f.backdrop_border:SetAllPoints(f.backdrop) + f.backdrop_border:SetBackdrop(pfUI.backdrop_blizz_border) + f.backdrop_border:SetBackdropBorderColor(er, eg, eb , ea) + end + end +end + +-- [ Create Shadow ] +-- Creates a pfUI compatible frame as shadow element +-- 'f' [frame] the frame which should get a backdrop. +function pfUI.api.CreateBackdropShadow(f) + -- exit if now frame was given + if not f then return end + + if f.backdrop_shadow or pfUI_config.appearance.border.shadow ~= "1" then + return + end + + local anchor = f.backdrop or f + f.backdrop_shadow = CreateFrame("Frame", nil, anchor) + f.backdrop_shadow:SetFrameStrata("BACKGROUND") + f.backdrop_shadow:SetFrameLevel(1) + f.backdrop_shadow:SetPoint("TOPLEFT", anchor, "TOPLEFT", -5, 5) + f.backdrop_shadow:SetPoint("BOTTOMRIGHT", anchor, "BOTTOMRIGHT", 5, -5) + f.backdrop_shadow:SetBackdrop(pfUI.backdrop_shadow) + f.backdrop_shadow:SetBackdropBorderColor(0, 0, 0, tonumber(pfUI_config.appearance.border.shadow_intensity)) +end + +-- [ Bar Layout Options ] -- +-- 'barsize' size of bar in number of buttons +-- returns: array of options as strings for pfUI.gui.bar +function pfUI.api.BarLayoutOptions(barsize) + assert(barsize > 0 and barsize <= NUM_ACTIONBAR_BUTTONS,"BarLayoutOptions: barsize "..tostring(barsize).." is invalid") + local options, seen = {}, {} + local function add(option) + if not seen[option] then table.insert(options, option); seen[option] = true end + end + for i,layout in ipairs(pfGridmath[barsize]) do + add(string.format("%d x %d",layout[1],layout[2])) + end + for _, option in ipairs({ + "1 x 12", "1 x 10", "2 x 10", "2 x 5", "3 x 10", "3 x 5", "3 x 3", + "5 x 3", "10 x 3", "5 x 2", "10 x 2", "10 x 1", "12 x 1" + }) do add(option) end + table.sort(options, function(a, b) + local _, _, ac, ar = string.find(a, "(%d+)%s*x%s*(%d+)") + local _, _, bc, br = string.find(b, "(%d+)%s*x%s*(%d+)") + ac, ar = tonumber(ac) or 1, tonumber(ar) or 1 + bc, br = tonumber(bc) or 1, tonumber(br) or 1 + local ah = ac >= ar and 0 or 1 + local bh = bc >= br and 0 or 1 + if ah ~= bh then return ah < bh end + if ah == 0 then if ac ~= bc then return ac > bc end; return ar < br + else if ar ~= br then return ar > br end; return ac < bc end + end) + return options +end + +-- [ Bar Layout Formfactor ] -- +-- 'option' string option as used in pfUI_config.bars[bar].option +-- returns: integer formfactor +local formfactors = {} +setmetatable(formfactors, {__mode = "v"}) -- weak table so values not referenced are collected on next gc +function pfUI.api.BarLayoutFormfactor(option) + if formfactors[option] then + return formfactors[option] + else + for barsize,_ in ipairs(pfGridmath) do + local options = pfUI.api.BarLayoutOptions(barsize) + for i,opt in ipairs(options) do + if opt == option then + formfactors[option] = i + return formfactors[option] + end + end + end + end +end + +local function ResolveBarLayout(barsize, formfactor, uneven) + local layout = tostring(formfactor or "") + local _, _, a, b = string.find(layout, "(%d+)%s*x%s*(%d+)") + if a and b then + local cols, rows = tonumber(a), tonumber(b) + cols = math.min(NUM_ACTIONBAR_BUTTONS, math.max(1, cols)) + rows = math.min(NUM_ACTIONBAR_BUTTONS, math.max(1, rows)) + local orientation = string.upper(uneven or "DOWN") + local mode = (orientation == "LEFT" or orientation == "RIGHT") and "cols" or "rows" + if mode == "rows" then + cols = math.max(1, math.min(cols, barsize)) + rows = math.max(1, math.ceil(barsize / cols)) + else + rows = math.max(1, math.min(rows, barsize)) + cols = math.max(1, math.ceil(barsize / rows)) + end + return cols, rows, mode, orientation + end + local index = pfUI.api.BarLayoutFormfactor(layout) + if not index then return nil end + local cols, rows = unpack(pfGridmath[barsize][index]) + return cols, rows, "rows", "DOWN" +end + +-- [ Bar Layout Size ] -- +-- 'bar' frame reference, +-- 'barsize' integer number of buttons, +-- 'formfactor' string formfactor in cols x rows, +-- 'padding' the spacing between buttons +function pfUI.api.BarLayoutSize(bar,barsize,formfactor,iconsize,bordersize,padding,uneven,fillmode) + assert(barsize > 0 and barsize <= NUM_ACTIONBAR_BUTTONS,"BarLayoutSize: barsize "..tostring(barsize).." is invalid") + local cols, rows = ResolveBarLayout(barsize, formfactor, uneven) + if not cols or not rows then cols, rows = unpack(pfGridmath[barsize][1]) end + local width = (iconsize + bordersize*2+padding) * cols + padding + local height = (iconsize + bordersize*2+padding) * rows + padding + bar._size = {width,height} + return bar._size +end + +-- [ Bar Button Anchor ] -- +-- 'button' frame reference +-- 'basename' name of button frame without index +-- 'buttonindex' index number of button on bar +-- 'formfactor' string formfactor in cols x rows +-- 'iconsize' size of the button +-- 'bordersize' default bordersize +-- 'padding' the spacing between buttons +function pfUI.api.BarButtonAnchor(button,basename,buttonindex,barsize,formfactor,iconsize,bordersize,padding,uneven,fillmode) + assert(barsize > 0 and barsize <= NUM_ACTIONBAR_BUTTONS,"BarButtonAnchor: barsize "..tostring(barsize).." is invalid") + local cols, rows, mode, orientation = ResolveBarLayout(barsize, formfactor, uneven) + if not cols or not rows then + cols, rows, mode, orientation = unpack(pfGridmath[barsize][1]), "rows", "DOWN" + end + + -- fillmode: remap buttonindex to slotindex (grid position in native fill order) + -- only needed when fill direction differs from layout mode + local slotindex = buttonindex + if fillmode == "row" and mode == "cols" then + local full = math.floor(barsize / rows) + local rem = barsize - full * rows + local n = 0 + for r = 1, rows do + local w = full + ((r <= rem) and 1 or 0) + if n + w >= buttonindex then + local c = buttonindex - n + slotindex = (c - 1) * rows + r + break + end + n = n + w + end + elseif fillmode == "col" and mode == "rows" then + local full = math.floor(barsize / cols) + local rem = barsize - full * cols + local n = 0 + for c = 1, cols do + local h = full + ((c <= rem) and 1 or 0) + if n + h >= buttonindex then + local r = buttonindex - n + slotindex = (r - 1) * cols + c + break + end + n = n + h + end + end + + local parent = button:GetParent() + local step = iconsize + bordersize*2 + padding + local row, col + if mode == "cols" then + col = math.ceil(slotindex / rows) + row = slotindex - ((col-1) * rows) + else + row = math.ceil(slotindex / cols) + col = slotindex - ((row-1) * cols) + end + if mode == "cols" and orientation == "LEFT" then + local final_col = math.ceil(barsize / rows) + local count = barsize - (final_col - 1) * rows + if count > 0 and count < rows then + if col == final_col then col = 1 else col = col + 1 end + end + elseif mode == "rows" and orientation == "UP" then + local final_row = math.ceil(barsize / cols) + local count = barsize - (final_row - 1) * cols + if count > 0 and count < cols then + if row == final_row then row = 1 else row = row + 1 end + end + end + local x = bordersize + padding + (col - 1) * step + local y = -bordersize - padding - (row - 1) * step + if slotindex == 1 then + button._anchor = {"TOPLEFT", parent, "TOPLEFT", x, y} + else + button._anchor = {"TOPLEFT", parent, "TOPLEFT", x, y} + if mode == "cols" then + local final_col = math.ceil(barsize / rows) + local count = barsize - (final_col - 1) * rows + local target_col = (orientation == "LEFT") and 1 or cols + if count > 0 and count < rows and col == target_col and row == 1 then + button._anchor[5] = button._anchor[5] - (rows - count) * step / 2 + end + else + local final_row = math.ceil(barsize / cols) + local count = barsize - (final_row - 1) * cols + local target_row = (orientation == "UP") and 1 or rows + if count > 0 and count < cols and row == target_row and col == 1 then + button._anchor[4] = button._anchor[4] + (cols - count) * step / 2 + end + end + end + return button._anchor +end + +-- [ Enable Autohide ] -- +-- 'frame' the frame that should be hidden +function pfUI.api.EnableAutohide(frame, timeout, combat) + if not frame then return end + local timeout = timeout + + frame.hover = frame.hover or CreateFrame("Frame", frame:GetName() .. "Autohide", frame) + frame.hover:SetParent(frame) + frame.hover:SetAllPoints(frame) + frame.hover.parent = frame + frame.hover:Show() + + if combat then + frame.hover:RegisterEvent("PLAYER_REGEN_ENABLED") + frame.hover:RegisterEvent("PLAYER_REGEN_DISABLED") + frame.hover:SetScript("OnEvent", function() + if event == "PLAYER_REGEN_DISABLED" then + this.parent:SetAlpha(1) + this.activeTo = "keep" + elseif event == "PLAYER_REGEN_ENABLED" then + this.activeTo = GetTime() + timeout + end + end) + end + + frame.hover:SetScript("OnUpdate", function() + -- throttle to 0.05s + if (this.tick or 0) > GetTime() then return end + this.tick = GetTime() + 0.05 + + if this.activeTo == "keep" then return end + + if MouseIsOver(this, 10, -10, -10, 10) then + this.activeTo = GetTime() + timeout + this.parent:SetAlpha(1) + elseif this.activeTo then + if this.activeTo < GetTime() and this.parent:GetAlpha() > 0 then + local fps = (60 / math.max(GetFramerate(), 1)) + this.parent:SetAlpha(this.parent:GetAlpha() - 0.05*fps) + end + else + this.activeTo = GetTime() + timeout + end + end) +end + +-- [ Disable Autohide ] -- +-- 'frame' the frame that should get the autohide removed +function pfUI.api.DisableAutohide(frame) + if not frame then return end + if not frame.hover then return end + + frame.hover:SetScript("OnEvent", nil) + frame.hover:SetScript("OnUpdate", nil) + frame.hover:Hide() + frame:SetAlpha(1) +end + +-- [ GetColoredTime ] -- +-- 'remaining' the time in seconds that should be converted +-- return a colored string including a time unit (m/h/d) +local color_day, color_hour, color_minute, color_low, color_normal +function pfUI.api.GetColoredTimeString(remaining) + if not remaining then return "" end + + -- Show days if remaining is > 99 Hours (99 * 60 * 60) + if remaining > 356400 then + if not color_day then + local r,g,b,a = pfUI.api.GetStringColor(C.appearance.cd.daycolor) + color_day = pfUI.api.rgbhex(r,g,b) + end + + return color_day .. round(remaining / 86400) .. "|rd" + + -- Show hours if remaining is > 99 Minutes (99 * 60) + elseif remaining > 5940 then + if not color_hour then + local r,g,b,a = pfUI.api.GetStringColor(C.appearance.cd.hourcolor) + color_hour = pfUI.api.rgbhex(r,g,b) + end + + return color_hour .. round(remaining / 3600) .. "|rh" + + -- Show minutes if remaining is > 99 Seconds (99) + elseif remaining > 99 then + if not color_minute then + local r,g,b,a = pfUI.api.GetStringColor(C.appearance.cd.minutecolor) + color_minute = pfUI.api.rgbhex(r,g,b) + end + + return color_minute .. round(remaining / 60) .. "|rm" + + -- Show milliseconds on low + elseif remaining <= 5 and pfUI_config.appearance.cd.milliseconds == "1" then + if not color_low then + local r,g,b,a = pfUI.api.GetStringColor(C.appearance.cd.lowcolor) + color_low = pfUI.api.rgbhex(r,g,b) + end + + return color_low .. string.format("%.1f", round(remaining,1)) + + -- Show seconds on low + elseif remaining <= 5 then + if not color_low then + local r,g,b,a = pfUI.api.GetStringColor(C.appearance.cd.lowcolor) + color_low = pfUI.api.rgbhex(r,g,b) + end + + return color_low .. round(remaining) + + -- Show seconds on normal + elseif remaining >= 0 then + if not color_normal then + local r, g, b, a = pfUI.api.GetStringColor(C.appearance.cd.normalcolor) + color_normal = pfUI.api.rgbhex(r,g,b) + end + return color_normal .. round(remaining) + + -- Return empty + else + return "" + end +end + +-- [ GetColorGradient ] -- +-- 'perc' percentage (0-1) +-- return r,g,b and hexcolor +local gradientcolors = {} +function pfUI.api.GetColorGradient(perc) + if not perc or perc ~= perc then perc = 0 end -- NaN guard: NaN ~= NaN is true in Lua + perc = perc > 1 and 1 or perc + perc = perc < 0 and 0 or perc + perc = floor(perc*100)/100 + + local index = perc + if not gradientcolors[index] then + local r1, g1, b1, r2, g2, b2 + + if perc <= 0.5 then + perc = perc * 2 + r1, g1, b1 = 1, 0, 0 + r2, g2, b2 = 1, 1, 0 + else + perc = perc * 2 - 1 + r1, g1, b1 = 1, 1, 0 + r2, g2, b2 = 0, 1, 0 + end + + local r = round(r1 + (r2 - r1) * perc, 4) + local g = round(g1 + (g2 - g1) * perc, 4) + local b = round(b1 + (b2 - b1) * perc, 4) + local h = pfUI.api.rgbhex(r,g,b) + + gradientcolors[index] = {} + gradientcolors[index].r = r + gradientcolors[index].g = g + gradientcolors[index].b = b + gradientcolors[index].h = h + end + + return gradientcolors[index].r, + gradientcolors[index].g, + gradientcolors[index].b, + gradientcolors[index].h +end + +-- [ GetNoNameObject ] -- +-- 'frame' [string] parent frame +-- 'scantype' [string] scanning Region or Children +-- 'objtype' [string] +-- 'layer' [string] +-- 'arg1' [string] +-- return object +function pfUI.api.GetNoNameObject(frame, objtype, layer, arg1, arg2) + local arg1 = arg1 and gsub(arg1, "([%+%-%*%(%)%?%[%]%^])", "%%%1") + local arg2 = arg2 and gsub(arg2, "([%+%-%*%(%)%?%[%]%^])", "%%%1") + + local objects + if objtype == "Texture" or objtype == "FontString" then + objects = {frame:GetRegions()} + else + objects = {frame:GetChildren()} + end + + for _, object in ipairs(objects) do + local check = true + if object:GetObjectType() ~= objtype or (layer and object:GetDrawLayer() ~= layer) then check = false end + + if check then + if objtype == "Texture" and object.SetTexture and object:GetTexture() ~= "Interface\\BUTTONS\\WHITE8X8" then + if arg1 then + local texture = object:GetTexture() + if texture and not string.find(texture, arg1, 1) then check = false end + end + + if check then return object end + elseif objtype == "FontString" and object.SetText then + if arg1 then + local text = object:GetText() + if text and not string.find(text, arg1, 1) then check = false end + end + + if check then return object end + elseif objtype == "Button" and object.GetNormalTexture and object:GetNormalTexture() then + if arg1 then + local texture = object:GetNormalTexture():GetTexture() + if texture and not string.find(texture, arg1, 1) then check = false end + end + if arg2 then + local text = object:GetText() + if text and not string.find(text, arg2, 1) then check = false end + end + + if check then return object end + end + end + end +end + +-- [ TryMemoizedFuncLoadstringForSpellCasts ] +-- Memoizes lua function strings for spell casts to improve performance. +-- Supports both string functions and direct function values. +-- 'funcOrStr' [function|string] Either a function or a lua string to execute +-- return: [function|nil] The function to execute, or nil on error +local memoizedFuncs = {} +function pfUI.api.TryMemoizedFuncLoadstringForSpellCasts(funcOrStr) + -- If it's already a function, return it directly + if type(funcOrStr) == "function" then + return funcOrStr + end + + -- If it's a string, try to memoize it + if type(funcOrStr) == "string" then + -- Check if we've already compiled this string + if memoizedFuncs[funcOrStr] then + return memoizedFuncs[funcOrStr] + end + + -- Try to compile the string + local func = loadstring(funcOrStr) + if func then + memoizedFuncs[funcOrStr] = func + return func + end + end + + return nil +end \ No newline at end of file