You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1371 lines
42 KiB

-- Neuron is a World of Warcraft® user interface addon.
-- Copyright (c) 2017-2021 Britt W. Yazel
-- Copyright (c) 2006-2014 Connor H. Chenoweth
-- This code is licensed under the MIT license (see LICENSE for details)
local _, addonTable = ...
local Neuron = addonTable.Neuron
local ActionButton = Neuron.ActionButton
local Bar = Neuron.Bar
local petIcons = {}
--[[ Item Cache ]]
local bagsToCache = {[0]=true,[1]=true,[2]=true,[3]=true,[4]=true,["Worn"]=true }
local timerTimes = {} -- indexed by arbitrary name, the duration to run the timer
local timersRunning = {} -- indexed numerically, timers that are running
local barsToUpdate = {}
local FOBarIndex, FOBtnIndex, AnchorIndex = {}, {}, {}
Neuron.FOBarIndex = FOBarIndex
Neuron.FOBtnIndex = FOBtnIndex
Neuron.AnchorIndex = AnchorIndex
--[[ Timer Management ]]
local timerFrame
--I think this is only used in Neuron-Flyouts
local POINTS = {
R = "RIGHT",
L = "LEFT",
T = "TOP",
B = "BOTTOM",
TL = "TOPLEFT",
TR = "TOPRIGHT",
BL = "BOTTOMLEFT",
BR = "BOTTOMRIGHT",
C = "CENTER",
RIGHT = "RIGHT",
LEFT = "LEFT",
TOP = "TOP",
BOTTOM = "BOTTOM",
TOPLEFT = "TOPLEFT",
TOPRIGHT = "TOPRIGHT",
BOTTOMLEFT = "BOTTOMLEFT",
BOTTOMRIGHT = "BOTTOMRIGHT",
CENTER = "CENTER"
}
------------------------------------------------------------------------------
-----------------------------
---helper funcs--------------
-----------------------------
-- returns true if arg and compareTo match. arg is a [Cc][Aa][Ss][Ee]-insensitive pattern
-- so we can't equate them and to get an exact match we need to append ^ and $ to the pattern
local function compare(arg,compareTo,exact)
return compareTo:match(format("^%s$",arg)) and true
end
--- Sorting function
local function keySort(list)
local array = {}
local i = 0
for n in pairs(list) do
table.insert(array, n)
end
table.sort(array)
local sorter = function()
i = i + 1
if array[i] == nil then
return nil
else
return array[i], list[array[i]]
end
end
return sorter
end
local function timerFrame_OnUpdate(frame, elapsed)
local tick
local times = timerTimes
local timers = timersRunning
for i=#timers,1,-1 do
local func = timers[i]
times[func] = times[func] - elapsed
if times[func] < 0 then
table.remove(timers,i)
func()
end
tick = true
end
if not tick then
frame:Hide()
end
end
---@param needles table<number,string>: list of strings
---@param haystack string - a string to check in
---@return boolean
local function isAllMatchIn(needles,haystack)
if haystack == "" then return false end
local numNeedles, hit = 0, 0
for k,_ in pairs(needles) do
numNeedles = numNeedles + 1
if haystack:match(k) then
hit = hit + 1
end
end
return hit == numNeedles
end
---@param needles: list of strings
---@param haystack: string - a string to check in
---@return boolean
local function isAnyMatchIn(needles,haystack)
if haystack == "" then return false end
local numNeedles, hit = 0, false
for k,_ in pairs(needles) do
numNeedles = numNeedles + 1
if haystack:match(k) then
hit = true -- mark as hit
break
end
end
if numNeedles < 1 then return false end
return hit
end
---@param index number,
---@param bookType string constant ("spell" or "pet")
---@return string, string, string, number, number name, rank|subType, spellType, spellID, icon
local function getSpellInfo(index, bookType)
local spellBookSpellName, spellRankOrSubtype = GetSpellBookItemName(index, bookType)
local spellType,spellIdOrActionId = GetSpellBookItemInfo(index, bookType)
local _,_, icon = GetSpellInfo(index, spellRankOrSubtype)
return spellBookSpellName, spellRankOrSubtype, spellType, spellIdOrActionId, icon
end
local function sequence(delim,first,...)
local head, tail = {first}, {...}
local switch =
{
["string"]=function(c,d,n) return c..(d and d or "")..n end,
["table"]=function(c,d,n) if #c == 0 then for k,v in pairs(n) do for _k,_v in pairs(d and d or {}) do c[_k] = _v end c[k]=v end else for _,v in ipairs(n) do for _,_v in ipairs(d and d or {}) do table.insert(c,_v) end table.insert(c,v) end end return c end,
["number"]=function(c,d,n) return c + (d and d or 0) + n end,
["function"]= function(c,d,n) return function(...) local args = {...} return c(args[1]),d and d(args[2]),n(args[3]) end end,
--["CFunction"]= function(c,d,n) return function(...) return c(...),d and d(...), n(...) end end,
["boolean"] = function(c,d,n) return c and ((d or n) and n) end,
["userdata"]=function(...) print("sequence for type userdata is not implemented") return end,
["thread"] = function(...) print("sequence for type thread is not implemented") return end,
["nil"] = function(...) return end,
}
while #tail > 0 do
if type(head[#head])~=type(tail[1]) or (delim and type(delim)~=type(head[#head])) then return end
head={switch[type(head[#head])](head[#head],delim,tail[1])}
table.remove(tail,1)
end
return table.unpack(head)
end
---Creates a list of Criteria based on user provided keys and the keys prefixes to match against or a default setting if none are provided.
---
---Returns the list of Criteria and the user provided keys, now been sorted, in the form of : ...
---Criteria: { Must:CriteriaRule: { Match:CriteriaMatch: {Key:string,boolean}, MatchCount:CriteriaMatchCount: number},...}, keys: string
---(default rules besides
---
---@overload fun():Criteria,string same as fun(true)
---@overload fun(useDefault:boolean):Criteria,string
---@overload fun(rules:CriteriaRule):Criteria,string
---@overload fun(rules:nil,negativeRules:CriteriaRule):Criteria,string
---@overload fun(rules:CriteriaRule,useDefault:boolean):Criteria,string
---@overload fun(rules:CriteriaRule,negativeRules:CriteriaRule):(Criteria,string)
---@overload fun(rules:CriteriaRule,negativeRules:CriteriaRule,useDefault:boolean):(Criteria,string)
---@return (Criteria, string)
---@param keyPrefixes string|table<number,table<string,Key>> keys as a comma separated string or list of name,value pairs. Matched keys have value: true.
---@param negativeKeyPrefixes string|table<number,table<string,Key>> keys as a comma separated string or list of name,value pairs. Matched keys have value: false.
---@param useDefault boolean flag whether or not to use default rules (table<name:string,value:Key: string> {"MustNot","!"}{"Optional","~"}{"Slot","#"})
function ActionButton:getCriteria(rules,negativeRules, useDefault)
if type(rules) == "boolean" and negativeRules == nil and useDefault == nil then
useDefault = rules
rules = nil
elseif type(negativeRules) == "boolean" and useDefault == nil then
useDefault = negativeRules
negativeRules = nil
end
if not (rules == nil and negativeRules == nil and useDefault == nil) then
local ok = true
if rules and (not type(rules) == "string" or type(rules) == "table") then
print("getCriteria expects rules parameter to be a string or a list table")
ok = false
end
if negativeRules and (not type(negativeRules) == "string" or type(negativeRules) == "table") then
print("getCriteria expects negativeRules parameter to be a string or a list table")
ok = false
end
if useDefault == nil and type(useDefault) ~= "boolean" then
print("getCriteria expects useDefault parameter to be a boolean")
ok =false
end
if not ok then return end
end
---@alias Key string
---@alias CriteriaMatchCount number
---@alias CriteriaMatch table<Key,boolean>
---@alias CriteriaRule string|table<number,table<string,Key>>
---@alias Criteria table<CriteriaRule,CriteriaMatch|CriteriaMatchCount>
---@type Criteria
local result = {}
function result:TypeCheckOK()
local ok = not type(self.params[1]) == "string" or (type(self.params[1]) == "table" and type(self.params[1].name) == "string" and type(self.params[1].value) == "string")
return ok
end
function result:defineCriteria(rule, ckey, val)
self.params = {rule,ckey}
if not self:TypeCheckOK() then return end
local cmd, arg = (ckey):match("%s*(%p*)(%P+)")
arg = arg:lower()
if not self[rule.name] then
---@type CriteriaRule
self[rule.name] = {}
---@type CriteriaMatchCount
self[rule.name].MatchCount = 0
end
if (rule.value == "" and cmd == "") or (rule.value ~= "" and cmd:match(rule.value)) then
if not self[rule.name].Match then
---@type CriteriaMatch
self[rule.name].Match = {}
---@type Key
self[rule.name].Match[arg] = false
end
if self[rule.name].Match[arg] then -- added_or_positive ? negate : add.
self[rule.name].Match[arg] = false
else
self[rule.name].Match[arg] = val
self[rule.name].MatchCount = self[rule.name].MatchCount + 1
end
end
end
local keys, rulesInOrder, defaultRules = self.flyout.keys, {}, {{name="Optional",value="~"},{name="Slot",value="#"},"break",{name="MustNot",value="!"}}
if negativeRules and type(rules) == type(negativeRules) then
rulesInOrder = sequence(type(rules) == "string" and ",break," or {"break"} ,rules,negativeRules)
elseif negativeRules then
local toConvert, first, second = type(rules) == "string" and rules or negativeRules, {},{}
rulesInOrder = {}
for rule in gmatch(toConvert) do
table.insert(rulesInOrder,{["name"]=rule,["value"]=rule})
end
first = type(rules) == "string" and rulesInOrder or rules
second = type(rules) == "string" and negativeRules or rulesInOrder
rulesInOrder = sequence({"break"},first,second)
end
for ckey in gmatch(keys, "[^,]+") do -- sort requirements
local positive = true
if type(rulesInOrder) == "string" then
for rule in gmatch(rulesInOrder,"[^,]") do
if rule == "break" then positive = false else
result:defineCriteria(rule,ckey,positive)
end
end
else
for _,rule in ipairs(rulesInOrder) do
if rule == "break" then positive = false else
result:defineCriteria(rule,ckey,positive)
end
end
end
-- defalt behaviour
if useDefault or (not rules and not negativeRules) then
for _, rule in ipairs(defaultRules) do
if rule == "break" then positive = false else
result:defineCriteria(rule,ckey,positive)
end
end
end
result:defineCriteria({name="Must",value=""},ckey,true)
end
result.params = nil;
return result, keys
end
---@param Criteria Criteria - Default search criteria, (Rule names: MustNot, Optional and Slot)
---@param haystack string - string to search for
---@param index number - an index number, which Slot # to match against.
local function isMatch(Criteria, haystack, index)
local hit = false
if (Criteria.MustNot.MatchCount > 0 and isAnyMatchIn(Criteria.MustNot.Match,haystack:lower())) or (index ~= nil and (Criteria.Slot.MatchCount > 0 and not Criteria.Slot.Match[""..index])) then
return hit
end
if (Criteria.Must.MatchCount == 0 and Criteria.Optional.MatchCount > 0 and isAnyMatchIn(Criteria.Optional.Match, haystack:lower())) then
hit = true
end
if (Criteria.Must.MatchCount > 0 and isAllMatchIn(Criteria.Must.Match,haystack:lower())) then
if (Criteria.Optional.MatchCount > 0) and not isAnyMatchIn(Criteria.Optional.Match, haystack:lower()) then return false end
hit = true
end
return hit
end
--- Filter handler for items
-- item:id will get all items of that itemID
-- item:name will get all items that contain "name" in its name
function ActionButton:filter_item(tooltip)
local data, itemTooltips, Criteria = {},{}, self:getCriteria()
-- build tooltip table
if (tooltip) then -- part I of tooltip cache version
for i,v in pairs(bagsToCache) do
if (tostring(i)):match("Worn") and v then --items worn
for j=0, 19 do -- go through equip slots
local itemId = GetInventoryItemID("player",j)
if (itemId) then
GameTooltip:SetOwner(UIParent,"ANCHOR_NONE")
GameTooltip:SetInventoryItem("player",j)
local itemTooltip = ""
for l=GameTooltip:NumLines(),2,-1 do
local text = _G["GameTooltipTextLeft"..l]:GetText()
if (text) then
itemTooltip = text.." "..itemTooltip
end
end
itemTooltips[i..":"..j] = "worn "..itemTooltip
end
end
else --bags
for j=1, GetContainerNumSlots(i) do
local itemId = GetContainerItemID(i,j)
if (itemId) then
GameTooltip:SetOwner(UIParent,"ANCHOR_NONE")
GameTooltip:SetBagItem(i,j)
local itemTooltip = ""
for l=GameTooltip:NumLines(),2,-1 do
local text = _G["GameTooltipTextLeft"..l]:GetText()
if (text) then
itemTooltip = text.." "..itemTooltip
end
end
itemTooltips[i..":"..j] = itemTooltip
end
end
end
end
end
-- perform checks
for i,v in pairs(bagsToCache) do -- Go through bags
if (tostring(i)):match("Worn") and v then -- items worn
for j=0, 19 do -- go through equip slots
local itemId = GetInventoryItemID("player",j)
if (itemId and itemId ~= 0) then
local name,_,_,_,_,_,_,_,equipLoc = GetItemInfo(itemId)
if name then
repeat -- repeat until true gives breaks of the repeat the functionality of a C continue
if tooltip then
-- we built the index on the same logic so we know itemTooltips[i..":"..j] exists.
local findIn = name.." "..itemTooltips[i..":"..j]
if (isMatch(Criteria,findIn,j)) then
data[name] = "item"
end
else -- match by name
local findIn = "worn "..name
if (isMatch(Criteria,findIn,j)) then
data[name] = "item"
end
end
until true
if (data[name] and (not Neuron.itemCache[name])) then
Neuron.itemCache[name] = itemId -- if it isn't in the items cache the icon and tooltip won't show.
end
end
end
end
else -- bags
for j=1, GetContainerNumSlots(i) do
local itemId = GetContainerItemID(i,j)
if (itemId) then
local name = GetItemInfo(itemId)
--itemName, itemLink, itemRarity, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount,
--itemEquipLoc, itemIcon, itemSellPrice, itemClassID, itemSubClassID, bindType, expacID, itemSetID,
--isCraftingReagent = GetItemInfo(itemID or "itemString" or "itemName" or "itemLink")
if name then
repeat -- repeat until true gives breaks of the repeat the functionality of a C continue
if tooltip then
-- we built the index on the same logic so we know itemTooltips[i..":"..j] exists.
local findIn = name.." "..itemTooltips[i..":"..j]
--TODO: Get item equipable slot, this j is wrong
if (isMatch(Criteria,findIn,j)) then
data[name] = "item"
end
else -- match by name
if (isMatch(Criteria,name,j)) then
data[name] = "item"
end
end
until true
if (data[name] and not Neuron.itemCache[name]) then
Neuron.itemCache[name] = itemId -- if it isn't in the items cache the icon and tooltip won't show.
end
end
end
end
end
end
return data
end
--- Filter Handler for Spells
-- spell:id will get all spells of that spellID
-- spell:name will get all spells that contain "name" in its name or its flyout parent
function ActionButton:filter_spell(tooltip)
local data, spellTooltips, Criteria = {},{}, self:getCriteria()
-- build tooltip table
if (tooltip) then
for i=1, GetNumSpellTabs() do
local _,_,numSpellsInPrevTabs,entries = GetSpellTabInfo(i)
for j=numSpellsInPrevTabs + 1, numSpellsInPrevTabs + entries do -- go through entries
local bookType = BOOKTYPE_SPELL
if i == 5 and HasPetSpells() then
bookType = BOOKTYPE_PET --assumed a fifth tab is a pet tab.
end
local name, rank, spellType, spellID, icon = getSpellInfo(j,bookType)
repeat -- repeat until true gives breaks of the repeat the functionality of a C continue
if IsPassiveSpell(j,bookType) then break end
if not IsSpellKnown(spellID,bookType == BOOKTYPE_PET) then break end
if (("SPELL FUTURESPELL"):match(spellType) and spellID) then
GameTooltip:SetOwner(UIParent,"ANCHOR_NONE")
GameTooltip:SetSpellBookItem(j, spellType)
-- GameTooltip:SetSpellByID(spellIdOrActionId)
local spellTooltip = ""
for l=GameTooltip:NumLines(),2,-1 do
local text = _G["GameTooltipTextLeft"..l]:GetText()
if (text) then
spellTooltip = text.." "..spellTooltip
end
end
spellTooltips[i..":"..j] = spellTooltip
end
until true
end
end
end
-- perform checks
for i=1, GetNumSpellTabs() do -- Go through spell tabs
local _,_,numSpellsInPrevTabs,entries = GetSpellTabInfo(i)
for j=numSpellsInPrevTabs + 1, numSpellsInPrevTabs + entries do -- go through entries
local bookType = BOOKTYPE_SPELL
if i == 5 and HasPetSpells() then
bookType = BOOKTYPE_PET --assumed a fifth tab is a pet tab.
end
local name, rank, spellType, spellID, icon = getSpellInfo(j,bookType)
local searchName = name
if rank then
searchName = searchName.."("..rank..")"
end
repeat -- repeat until true gives breaks of the repeat the functionality of a C continue
if IsPassiveSpell(j,bookType) then break end
if not IsSpellKnown(spellID,bookType == BOOKTYPE_PET) then break end
if (("SPELL FUTURESPELL"):match(spellType) and spellID) then
if tooltip then
-- we built the index on the same logic so we know itemTooltips[i..":"..j] exists.
local findIn = searchName.." "..spellTooltips[i..":"..j]
if (isMatch(Criteria,findIn)) then
data[name] = "item"
end
else -- match by name
if (isMatch(Criteria,searchName)) then
data[name:lower()] = "item"
end
end
end
until true
if (data[name:lower()] and not (Neuron.spellCache[name:lower()] or Neuron.spellCache[name:lower().."()"])) then
-- if it isn't in the items cache the icon and tooltip won't show.
Neuron.spellCache[name:lower()] = { ["booktype"] = bookType,["index"] = j, ["spellType"] = spellType,["spellID"]= spellID, ["icon"]=icon,["spellName"]=name }
Neuron.spellCache[name:lower().."()"] = { ["booktype"] = bookType,["index"] = j, ["spellType"] = spellType,["spellID"]= spellID, ["icon"]=icon,["spellName"]=name }
end
end
end
return data
end
---Filter handler for item type
-- type:quest will get all quest items in bags, or those on person with Quest in a type field
-- type:name will get all items that have "name" in its type, subtype or slot name
function ActionButton:filter_type()
local data, itemTypes, Criteria = {},{}, self:getCriteria()
itemTypes = nil
--should this search tooltip by default? does the tooltip contain the type?
for i,v in pairs(bagsToCache) do -- Go through bags
if (tostring(i)):match("Worn") and v then -- items worn
for j=0, 19 do -- go through equip slots
local itemId = GetInventoryItemID("player",j)
if (itemId and itemId ~= 0) then
local name,_,_,_,_,itemType,itemSubType,_,equipLoc = GetItemInfo(itemId)
repeat -- repeat until true gives breaks of the repeat the functionality of a C continue
if (name) then -- match by name
local findIn = "worn "..itemType.." "..itemSubType
--TODO: Get item equipable slot, this j is wrong
if (isMatch(Criteria,findIn,j)) then
data[name] = "item"
end
end
until true
if (name and data[name] and (not Neuron.itemCache[name])) then
Neuron.itemCache[name] = itemId -- if it isn't in the items cache the icon and tooltip won't show.
end
end
end
else -- bags
for j=1, GetContainerNumSlots(i) do
local itemId = GetContainerItemID(i,j)
if (itemId) then
local name,_,_,_,_,itemType,itemSubType,_,itemSlot = GetItemInfo(itemId)
--itemName, itemLink, itemRarity, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount,
--itemEquipLoc, itemIcon, itemSellPrice, itemClassID, itemSubClassID, bindType, expacID, itemSetID,
--isCraftingReagent = GetItemInfo(itemID or "itemString" or "itemName" or "itemLink")
local isQuestItem, questID, isActive = false,false,false
if GetContainerItemQuestInfo then
isQuestItem, questID, isActive = GetContainerItemQuestInfo(i,j)
end
if name then
repeat -- repeat until true gives breaks of the repeat the functionality of a C continue
local findIn = name.." "..itemType.." "..itemSubType.." "..itemSlot.." "..((isQuestItem or questID or isActive) and "quest" or "")
if (isMatch(Criteria,findIn)) then
data[name] = "item"
end
until true
if (data[name] and not Neuron.itemCache[name]) then
Neuron.itemCache[name] = itemId -- if it isn't in the items cache the icon and tooltip won't show.
end
end
end
end
end
end
return data
end
--- Filter handler for mounts
-- mount:any, mount:flying, mount:land, mount:favorite, mount:fflying, mount:fland
-- mount:arg filters mounts that include arg in the name or arg="flying" or arg="land" or arg=="any"
function ActionButton:filter_mount()
local keys, found, mandatory, optional = self.flyout.keys, 0, 0, 0
local data = {}
for ckey in gmatch(keys, "[^,]+") do
local cmd, arg = (ckey):match("%s*(%p*)(%P+)")
local any = compare(string.lower(arg),"any")
local flying = compare(string.lower(arg),"flying")
local land = compare(string.lower(arg),"land")
local fflying = compare(string.lower(arg),"fflying") or compare(string.lower(arg),"favflying")
local fland = compare(string.lower(arg),"fland") or compare(string.lower(arg),"favland")
local favorite = compare(string.lower(arg),"favorite") or fflying or fland
arg = arg:lower()
for i,mountID in ipairs(C_MountJournal.GetMountIDs()) do
local mountName, mountSpellId, mountTexture, _, canSummon, _, isFavorite = C_MountJournal.GetMountInfoByID(mountID)
local spellName = GetSpellInfo(mountSpellId) -- sometimes mount name isn't same as spell name >:O
mountName = mountName:lower()
spellName = spellName:lower()
if mountName and canSummon then
local _,_,_,_,mountType = C_MountJournal.GetMountInfoExtraByID(mountID)
local canFly = mountType==247 or mountType==248
if favorite and isFavorite then
if (fflying and canFly) or (fland and not canFly) or (not fflying and not fland) then
data[spellName] = "spell"
end
elseif (flying and canFly) or (land and not canFly) then
data[spellName] = "spell"
elseif any or mountName:match(arg) or spellName:match(arg) then
data[spellName] = "spell"
end
end
end
end
return data
end
--- Filter handler for professions
--- not WoW Classic
-- profession:arg filters professions that include arg in the name or arg="primary" or arg="secondary" or arg="all"
function ActionButton:filter_profession()
local data = {}
-- runs func for each ...
local function RunForEach(func,...)
for i=1,select("#",...) do
func((select(i,...)))
end
end
local professions = {}
local keys, found, mandatory, optional = self.flyout.keys, 0, 0, 0
local profSpells = {}
for ckey in gmatch(keys, "[^,]+") do
local cmd, arg = (ckey):match("%s*(%p*)(%P+)")
RunForEach(function(entry) table.insert(professions,entry or false) end, GetProfessions())
local any = compare(string.lower(arg),"any")
local primaryOnly = compare(string.lower(arg),"primary")
local secondaryOnly = compare(string.lower(arg),"secondary")
arg = arg:lower()
for index,profession in pairs(professions) do
if profession then
local name, _, _, _, numSpells, offset = GetProfessionInfo(profession)
if (index<3 and primaryOnly) or (index>2 and secondaryOnly) or any or (name:lower()):match(arg) then
for i=1,numSpells do
local _, spellID = GetSpellBookItemInfo(offset+i,"professions")
local spellName = GetSpellInfo(spellID)
local isPassive = IsPassiveSpell(offset+i,"professions")
if not isPassive then
table.insert(profSpells, spellName:lower())
data[spellName:lower()] = "spell"
end
end
end
end
end
end
return data
end
--- Filter handler for companion pets
--- not WoW Classic
-- pet:arg filters companion pets that include arg in the name or arg="any" or arg="favorite(s)"
function ActionButton:filter_pet()
local data = {}
local keys = self.flyout.keys
for ckey in gmatch(keys, "[^,]+") do
local cmd, arg = (ckey):match("%s*(%p*)(%P+)")
local speciesID, _ = C_PetJournal.FindPetIDByName(arg)
local speciesName, speciesIcon = C_PetJournal.GetPetInfoBySpeciesID(speciesID)
if speciesName then
data[speciesName] = "companion"
petIcons[speciesName] = speciesIcon
end
end
return data
end
---Filter handler for toy items
--- not WoW Classic
-- toy:arg filters items from the toybox; arg="favorite" "any" or partial name
function ActionButton:filter_toy()
local keys, found, mandatory, optional = self.flyout.keys, 0, 0, 0
local data = {}
for ckey in gmatch(keys, "[^,]+") do
local cmd, arg = (ckey):match("%s*(%p*)(%P+)")
local any = compare(string.lower(arg),"any")
local favorite = compare(string.lower(arg),"favorite")
arg = arg:lower()
local name= GetItemInfo(arg)
if name then
data[name] = "item"
end
end
return data
end
--- Handler for Blizzard flyout spells
function ActionButton:GetBlizzData()
local data = {}
local visible, spellID, isKnown, petIndex, petName, spell, subName
local _, _, numSlots = GetFlyoutInfo(self.flyout.keys)
for i=1, numSlots do
visible = true
spellID, _, isKnown = GetFlyoutSlotInfo(self.flyout.keys, i)
petIndex, petName = GetCallPetSpellInfo(spellID)
if petIndex and (not petName or petName == "") then
visible = false
end
if isKnown and visible then
spell = GetSpellInfo(spellID)
data[spell] = "blizz"
end
end
return data
end
--- Flyout type handler
function ActionButton:GetDataList(options)
local tooltip
local scanData = {}
for types in gmatch(self.flyout.types, "%a+[%+]*") do
tooltip = types:match("%+")
if types:find("^b") then --Blizzard Flyout
scanData = self:GetBlizzData()
elseif types:find("^s") then --Spell
scanData = self:filter_spell(tooltip)
elseif types:find("^i") then --Item
scanData = self:filter_item(tooltip)
elseif types:find("^c") and Neuron.isWoWRetail then --Companion
scanData = self:filter_pet()
elseif types:find("^f") and Neuron.isWoWRetail then --toy
scanData = self:filter_toy()
elseif types:find("^m") then --Mount
scanData = self:filter_mount()
elseif types:find("^p") and Neuron.isWoWRetail then --Profession
scanData = self:filter_profession()
elseif types:find("^t") then --Item Type
scanData = self:filter_type()
end
end
return scanData
end
function ActionButton:updateFlyoutBars()
if not InCombatLockdown() then --Workaround for protected taint if UI reload in combat
local bar = table.remove(barsToUpdate) --this does nothing. It makes bar empty
if bar then
bar:SetObjectLoc()
bar:SetPerimeter()
bar:SetSize()
else
self:Hide()
end
end
end
function ActionButton:Flyout_UpdateData(init)
local slot
local pet = false
if self.flyout then
local count, list = 0, {}
local button, prefix, macroSet
local data = self:GetDataList(self.flyout.options)
for _,val in pairs(self.flyout.buttons) do
self:Flyout_ReleaseButton(val)
end
if data then
for spell, source in keySort(data) do
button = self:Flyout_GetButton()
button.source = source
if source == "spell" or source =="blizz" then
if spell:find("%(") then
button.macroshow = spell
else
button.macroshow = spell.."()"
end
button:SetAttribute("prefix", "/cast ")
button:SetAttribute("showtooltip", "#showtooltip "..button.macroshow.."\n")
prefix = "/cast "
elseif source == "companion" then
button.macroshow = spell
button:SetAttribute("prefix", "/summonpet ")
button:SetAttribute("showtooltip", "#showtooltip "..button.macroshow.."\n")
button:SetMacroName(spell)
button:SetAttribute("macro_Name", spell)
prefix = "/summonpet "
elseif source == "mount" then
button.macroshow = spell
button:SetAttribute("prefix", "/summonpet ")
button:SetAttribute("showtooltip", "#showtooltip "..button.macroshow.."\n")
prefix = "/summonpet "
elseif source == "item" then
if IsEquippableItem(spell) then
if self.flyout.keys:find("#%d+") then
slot = self.flyout.keys:match("%d+").." "
end
if slot then
prefix = "/equipslot "
button:SetAttribute("slot", slot.." ")
else
prefix = "/equip "
button:SetAttribute("prefix", "/equip ")
end
else
prefix = "/use "
button:SetAttribute("prefix", "/use ")
end
local itemname = GetItemInfo(spell)
button.macroshow = spell
button:SetMacroName(itemname)
button:SetAttribute("showtooltip", "#showtooltip "..button.macroshow.."\n")
if slot then
button:SetAttribute("showtooltip", "#showtooltip "..slot.."\n")
else
button:SetAttribute("showtooltip", "#showtooltip "..button.macroshow.."\n")
end
elseif source:find("equipset") then
button.macroshow = spell
button:SetMacroEquipmentSet(spell)
button:SetAttribute("prefix", "/equipset ")
button:SetAttribute("showtooltip", "")
prefix = "/equipset "
else
--should never get here
button.macroshow = ""
button:SetAttribute("prefix", "")
button:SetAttribute("showtooltip", "")
end
if slot then
button:SetAttribute("macro_Text", button:GetAttribute("prefix").."[nobtn:2] "..slot)
button:SetAttribute("*macrotext1", prefix.."[nobtn:2] "..slot..button.macroshow)
button:SetAttribute("flyoutMacro", button:GetAttribute("showtooltip")..button:GetAttribute("prefix").."[nobtn:2] "..slot.."\n/stopmacro [nobtn:2]\n/flyout "..self.flyout.options)
elseif pet then
button:SetAttribute("macro_Text", button:GetAttribute("prefix").."[nobtn:2] "..pet)
button:SetAttribute("*macrotext1", prefix.."[nobtn:2] "..pet)
button:SetAttribute("flyoutMacro", button:GetAttribute("showtooltip")..button:GetAttribute("prefix").."[nobtn:2] "..pet.."\n/stopmacro [nobtn:2]\n/flyout "..self.flyout.options)
else
button:SetAttribute("macro_Text", button:GetAttribute("prefix").."[nobtn:2] "..button.macroshow)
button:SetAttribute("*macrotext1", prefix.."[nobtn:2] "..button.macroshow)
button:SetAttribute("flyoutMacro", button:GetAttribute("showtooltip")..button:GetAttribute("prefix").."[nobtn:2] "..button.macroshow.."\n/stopmacro [nobtn:2]\n/flyout "..self.flyout.options)
end
if not macroSet and not self:GetMacroText():find("nobtn:2") then
self:SetMacroText(button:GetAttribute("flyoutMacro"))
macroSet = true
end
button:SetMacroText(button:GetAttribute("macro_Text"))
button:ClearButton()
button:UpdateAll()
list[#list+1] = button.id--table.insert(list, button.id)
count = count + 1
end
end
self.flyout.bar.objCount = count
self.flyout.bar.data.objectList = list
if not init then
table.insert(barsToUpdate, self.flyout.bar)
self:updateFlyoutBars()
end
end
end
function ActionButton:Flyout_UpdateBar()
self.FlyoutTop:Hide()
self.FlyoutBottom:Hide()
self.FlyoutLeft:Hide()
self.FlyoutRight:Hide()
local flyout = self.flyout
local pointA, pointB, hideArrow, shape, columns, pad
if flyout.shape and flyout.shape:lower():find("^c") then
shape = 2
else
shape = 1
end
if flyout.point then
pointA = flyout.point:match("%a+"):upper() pointA = POINTS[pointA] or "RIGHT"
end
if flyout.relPoint then
pointB = flyout.relPoint:upper() pointB = POINTS[pointB] or "LEFT"
end
if flyout.colrad and tonumber(flyout.colrad) then
if shape == 1 then
columns = tonumber(flyout.colrad)
elseif shape == 2 then
pad = tonumber(flyout.colrad)
end
end
if flyout.mode and flyout.mode:lower():find("^m") then
flyout.mode = "mouse"
else
flyout.mode = "click"
end
if flyout.hideArrow and flyout.hideArrow:lower():find("^h") then
hideArrow = true
end
if shape then
flyout.bar.data.shape = shape
else
flyout.bar.data.shape = 1
end
if columns then
flyout.bar.data.columns = columns
else
flyout.bar.data.columns = 12
end
if pad then
flyout.bar.data.padH = pad
flyout.bar.data.padV = pad
flyout.bar.data.arcStart = 0
flyout.bar.data.arcLength = 359
else
flyout.bar.data.padH = 0
flyout.bar.data.padV = 0
flyout.bar.data.arcStart = 0
flyout.bar.data.arcLength = 359
end
flyout.bar:ClearAllPoints()
flyout.bar:SetPoint(pointA, self, pointB, 0, 0)
flyout.bar:SetFrameStrata(self:GetFrameStrata())
flyout.bar:SetFrameLevel(self:GetFrameLevel()+1)
if not hideArrow then
if pointB == "TOP" then
self.flyout.arrowPoint = "TOP"
self.flyout.arrowX = 0
self.flyout.arrowY = 5
self.flyout.arrow = self.FlyoutTop
self.flyout.arrow:Show()
elseif pointB == "BOTTOM" then
self.flyout.arrowPoint = "BOTTOM"
self.flyout.arrowX = 0
self.flyout.arrowY = -5
self.flyout.arrow = self.FlyoutBottom
self.flyout.arrow:Show()
elseif pointB == "LEFT" then
self.flyout.arrowPoint = "LEFT"
self.flyout.arrowX = -5
self.flyout.arrowY = 0
self.flyout.arrow = self.FlyoutLeft
self.flyout.arrow:Show()
elseif pointB == "RIGHT" then
self.flyout.arrowPoint = "RIGHT"
self.flyout.arrowX = 5
self.flyout.arrowY = 0
self.flyout.arrow = self.FlyoutRight
self.flyout.arrow:Show()
end
end
self:Anchor_Update()
table.insert(barsToUpdate, flyout.bar)
self:updateFlyoutBars()
end
function ActionButton:Flyout_RemoveButtons()
for _,button in pairs(self.flyout.buttons) do
self:Flyout_ReleaseButton(button)
end
end
function ActionButton:Flyout_RemoveBar()
self.FlyoutTop:Hide()
self.FlyoutBottom:Hide()
self.FlyoutLeft:Hide()
self.FlyoutRight:Hide()
self:Anchor_Update(true)
self:Flyout_ReleaseBar(self.flyout.bar)
end
function ActionButton:UpdateFlyout(init)
-- temporarily disabling action buttons, since they are broken atm
if true then return end
local options
if self:GetMacroText() then
options = self:GetMacroText():match("/flyout%s(%C+)")
end
if self.flyout then
self:Flyout_RemoveButtons()
self:Flyout_RemoveBar()
end
if options then
if not self.flyout then
self.flyout = { buttons = {} }
end
self.flyout.bar = self:Flyout_GetBar()
self.flyout.options = options
self.flyout.types = select(1, (":"):split(options))
self.flyout.keys = select(2, (":"):split(options))
self.flyout.shape = select(3, (":"):split(options))
self.flyout.point = select(4, (":"):split(options))
self.flyout.relPoint = select(5, (":"):split(options))
self.flyout.colrad = select(6, (":"):split(options))
self.flyout.mode = select(7, (":"):split(options))
self.flyout.hideArrow = select(8, (":"):split(options))
self:Flyout_UpdateBar()
self:Flyout_UpdateData(init)
if not self.bar.watchframes then
self.bar.watchframes = {}
end
self.bar.watchframes[self.flyout.bar.handler] = true
AnchorIndex[self] = true
else
AnchorIndex[self] = nil
self.flyout = nil
end
end
function ActionButton:Flyout_ReleaseButton(button)
self.flyout.buttons[button.id] = nil
button.stored = true
button:SetMacroText()
button:SetMacroEquipmentSet()
button:SetMacroIcon()
button.macrospell = nil
button.macroitem = nil
button.macroshow = nil
button.macroBtn = nil
button.bar = nil
button:SetAttribute("*macrotext1", nil)
button:SetAttribute("flyoutMacro", nil)
button:ClearAllPoints()
button:SetPoint("CENTER")
button:Hide()
end
function ActionButton:Flyout_InitializeButtonSettings(bar)
if bar then
self.bar = bar
self.bar.data.tooltips = "normal"
end
self.Hotkey:Hide()
self.Name:Hide()
self:RegisterForClicks("AnyUp")
self:SetSkinned(true)
end
function ActionButton:Flyout_PostClick()
local button = self.anchor
button:SetMacroText(self:GetAttribute("flyoutMacro"))
button:SetMacroIcon(self:GetAttribute("macro_Icon") or nil)
button:SetMacroName(self:GetAttribute("macro_Name") or nil)
button:ClearButton()
button:UpdateAll()
self:UpdateStatus()
end
function ActionButton:Flyout_GetButton()
local id = 1
for _,button in ipairs(FOBtnIndex) do
if button.stored then
button.anchor = self
button.bar = self.flyout.bar
button.stored = false
self.flyout.buttons[button.id] = button
button:Show()
return button
end
id = id + 1
end
local newButton = CreateFrame("CheckButton", self:GetName().."_".."NeuronFlyoutButton"..id, UIParent, "NeuronActionButtonTemplate") --create the new button frame using the desired parameters
setmetatable(newButton, {__index = ActionButton})
newButton.elapsed = 0
newButton.class = "flyout"
newButton.id = id
newButton:SetID(0)
newButton:SetToplevel(true)
newButton.objTIndex = id
newButton.objType = "FlyoutButton"
newButton.data = { macro_Text = "" }
newButton.anchor = self
newButton.bar = self.flyout.bar
newButton.stored = false
SecureHandler_OnLoad(newButton)
newButton:SetAttribute("type1", "macro")
newButton:SetAttribute("*macrotext1", "")
newButton:SetScript("PostClick", function(self) self:Flyout_PostClick() end)
newButton:SetScript("OnEnter", function(self)
self:UpdateTooltip()
if self.flyout and self.flyout.arrow then
self.flyout.arrow:SetPoint(self.flyout.arrowPoint, self.flyout.arrowX/0.625, self.flyout.arrowY/0.625)
end
end)
newButton:SetScript("OnLeave", function(self)
GameTooltip:Hide()
if self.flyout and self.flyout.arrow then
self.flyout.arrow:SetPoint(self.flyout.arrowPoint, self.flyout.arrowX, self.flyout.arrowY)
end
end)
newButton:SetScript("OnShow", function(self) self:UpdateUsable(); self:UpdateIcon(); self:UpdateStatus() end)
newButton:SetScript("OnHide", function(self) self:UpdateUsable(); self:UpdateIcon(); self:UpdateStatus() end)
newButton:WrapScript(newButton, "OnClick", [[
local button = self:GetParent():GetParent()
button:SetAttribute("macroUpdate", true)
button:SetAttribute("*macrotext*", self:GetAttribute("flyoutMacro"))
self:GetParent():Hide()
]])
--link objects to their associated functions
newButton.InitializeButtonSettings = ActionButton.Flyout_InitializeButtonSettings
newButton:InitializeButtonSettings(self.flyout.bar)
newButton:Flyout_UpdateData(true)
newButton:SetSkinned(true)
newButton:Show()
self.flyout.buttons[id] = newButton
FOBtnIndex[id] = newButton
return newButton
end
function ActionButton:Flyout_ReleaseBar(bar)
self.flyout.bar = nil
bar.stored = true
bar:SetWidth(43)
bar:SetHeight(43)
bar:ClearAllPoints()
bar:SetPoint("CENTER")
if self.bar.watchframes then
self.bar.watchframes[bar.handler] = nil
end
end
function Bar:Flyout_OnEvent()
self:SetObjectLoc()
self:SetPerimeter()
self:SetSize()
end
function ActionButton:Flyout_GetBar()
local id = 1
for _,bar in ipairs(FOBarIndex) do
if bar.stored then
bar.stored = false
bar:SetParent(UIParent)
return bar
end
id = id + 1
end
local bar = CreateFrame("CheckButton", self:GetName().."_".."NeuronFlyoutBar"..id, UIParent, "NeuronBarTemplate")
setmetatable(bar, {__index = Neuron.Bar})
bar.class = "FlyoutBar"
bar.elapsed = 0
bar.data = { scale = 1 }
bar.Text:Hide()
bar.Message:Hide()
bar.MessageBG:Hide()
bar:SetID(id)
bar:SetWidth(43)
bar:SetHeight(43)
bar:SetFrameLevel(2)
bar:RegisterEvent("PLAYER_ENTERING_WORLD", "Flyout_OnEvent")
if not bar.data.objectList then
bar.data.objectList = {}
end
bar:Hide()
bar.handler = CreateFrame("Frame", "NeuronFlyoutHandler"..id, UIParent, "SecureHandlerStateTemplate, SecureHandlerShowHideTemplate")
bar.handler:SetAttribute("state-current", "homestate")
bar.handler:SetAttribute("state-last", "homestate")
bar.handler:SetAttribute("showstates", "homestate")
bar.handler:SetScript("OnShow", function() end)
bar.handler:SetAllPoints(bar)
bar.handler.bar = bar
bar.handler.elapsed = 0
--bar.handler:SetBackdrop({ bgFile = "Interface/Tooltips/UI-Tooltip-Background", edgeFile = "Interface/Tooltips/UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 12, insets = { left = 4, right = 4, top = 4, bottom = 4 } })
--bar.handler:SetBackdropColor(0,0,0,1)
--bar.handler:SetBackdropBorderColor(0,0,0,1)
----we need to activate all of these frames at least once. This place is as good as any I guess
--[[ Timer Management ]]
if not timerFrame then
timerFrame = CreateFrame("Frame") -- timer independent of main frame visibility
timerFrame:Hide()
timerFrame:SetScript("OnUpdate", timerFrame_OnUpdate)
end
bar.handler:Hide()
FOBarIndex[id] = bar
return bar
end
function ActionButton:Anchor_RemoveChild()
local child = self.flyout.bar and self.flyout.bar.handler
if child then
self:UnwrapScript(self, "OnEnter")
self:UnwrapScript(self, "OnLeave")
self:UnwrapScript(self, "OnClick")
self:SetAttribute("click-show", nil)
child:SetAttribute("timedelay", nil)
child:SetAttribute("_childupdate-onmouse", nil)
child:SetAttribute("_childupdate-onclick", nil)
child:UnwrapScript(child, "OnShow")
child:UnwrapScript(child, "OnHide")
end
end
function ActionButton:Anchor_UpdateChild()
local child = self.flyout.bar and self.flyout.bar.handler
if child then
local mode = self.flyout.mode
local delay
if mode == "click" then
self:SetAttribute("click-show", "hide")
self:WrapScript(self, "OnClick", [[
if button == "RightButton" then
if self:GetAttribute("click-show") == "hide" then
self:SetAttribute("click-show", "show")
else
self:SetAttribute("click-show", "hide")
end
control:ChildUpdate("onclick", self:GetAttribute("click-show"))
end
]])
child:WrapScript(child, "OnShow", [[
if self:GetAttribute("timedelay") then
self:RegisterAutoHide(self:GetAttribute("timedelay"))
else
self:UnregisterAutoHide()
end
]])
child:WrapScript(child, "OnHide", [[ self:GetParent():SetAttribute("click-show", "hide") self:UnregisterAutoHide() ]])
child:SetAttribute("timedelay", tonumber(delay) or 0)
child:SetAttribute("_childupdate-onclick", [[ if message == "show" then self:Show() else self:Hide() end ]] )
child:SetParent(self)
elseif mode == "mouse" then
self:WrapScript(self, "OnEnter", [[ control:ChildUpdate("onmouse", "enter") ]])
self:WrapScript(self, "OnLeave", [[ if not self:IsUnderMouse(true) then control:ChildUpdate("onmouse", "leave") end ]])
child:SetAttribute("timedelay", tonumber(delay) or 0)
child:SetAttribute("_childupdate-onmouse", [[ if message == "enter" then self:Show() elseif message == "leave" then self:Hide() end ]] )
child:WrapScript(child, "OnShow", [[
if self:GetAttribute("timedelay") then
self:RegisterAutoHide(self:GetAttribute("timedelay"))
else
self:UnregisterAutoHide()
end
]])
child:WrapScript(child, "OnHide", [[ self:UnregisterAutoHide() ]])
child:SetParent(self)
end
end
end
function ActionButton:Anchor_Update(remove)
if remove then
self:Anchor_RemoveChild()
else
self:Anchor_UpdateChild()
end
end