Sonomus: Initial commit.

git-svn-id: https://repos.wowace.com/wow/repairbroker/trunk@2 25fea357-c8a7-4792-97eb-cca2a7ef4644
This commit is contained in:
sonomus
2008-11-03 23:13:29 +00:00
parent dc5085a252
commit ef624564ba
10 changed files with 1192 additions and 0 deletions

6
.pkgmeta Normal file
View File

@ -0,0 +1,6 @@
package-as: RepairBroker
externals:
libs/LibTooltip-1.0: svn://svn.wowace.com/wow/libtooltip/mainline/trunk/LibTooltip-1.0
libs/CallbackHandler-1.0: svn://svn.wowace.com/wow/ace3/mainline/trunk/CallbackHandler-1.0
libs/LibStub: svn://svn.wowace.com/wow/libstub/mainline/tags/1.0

1
ReadMe.txt Normal file
View File

@ -0,0 +1 @@
Project page: http://www.wowace.com/projects/repairbroker/

208
RepairBroker.lua Normal file
View File

@ -0,0 +1,208 @@
local LibTooltip = LibStub('LibTooltip-1.0')
local LibDataBroker = LibStub('LibDataBroker-1.1')
if not LibDataBroker then return end
local name = "RepairBroker"
local Repair = LibDataBroker:NewDataObject(name, {
icon = "Interface\\Icons\\Trade_BlackSmithing",
label = "Dur",
text = "100%",
}
)
local print = function(msg) print("|cFF5555AA"..name..": |cFFAAAAFF"..msg) end
local slots = { }
do
local slotNames = { "HeadSlot", "ShoulderSlot", "ChestSlot", "WristSlot", "HandsSlot", "WaistSlot", "LegsSlot", "FeetSlot", "MainHandSlot", "SecondaryHandSlot", "RangedSlot" }
for i,name in ipairs(slotNames) do
slots[ i ] = { GetInventorySlotInfo(name), string.sub(name, 1, string.len(name)-4), 1 }
end
slotNames = nil -- dispose
end
local OnLoad = function()
if not RepairBrokerDB then
RepairBrokerDB = {
autoRepair = 1, -- nil or 1
useBuildBank = nil, -- nil or 1
}
end
end
---------------------------------
-- Support functions
---------------------------------
local DurabilityColor = function(perc)
if not perc or perc < 0 then return "|cFF555555" end
if perc == 1 then
return "|cFF005500" -- Dark green
elseif perc >= .9 then
return "|cFF00AA00" -- Green
elseif perc > .5 then
return "|cFFFFFF00" -- Yellow
elseif perc > .2 then
return "|cFFFF9900" -- Orange
else
return "|cFFFF0000" -- Red
end
end
local CopperToString = function(c)
local str = ""
if not c or c < 0 then return str end
if c > 10000 then
local g = math.floor(c/10000)
c = c - g*10000
str = str.."|cFFFFD800"..g.." |TInterface\\MoneyFrame\\UI-GoldIcon.blp:0:0:0:0|t "
end
if c > 100 then
local s = math.floor(c/100)
c = c - s*100
str = str.."|cFFC7C7C7"..s.." |TInterface\\MoneyFrame\\UI-SilverIcon.blp:0:0:0:0|t "
end
if c > 0 then
str = str.."|cFFEEA55F"..c.." |TInterface\\MoneyFrame\\UI-CopperIcon.blp:0:0:0:0|t "
end
return str
end
---------------------------------
-- Durability updates and repair
---------------------------------
local UpdateDurability = function()
local dur, max
local minDur = 1
for i,info in ipairs(slots) do
if GetInventoryItemLink("player", info[1]) then
dur, max = GetInventoryItemDurability(info[1])
if not dur or max == 0 then
info[3] = -1
else
info[3] = dur/max
if info[3] < minDur then minDur = info[3] end
end
else
info[3] = -1
end
end
Repair.text = DurabilityColor(minDur)..math.floor(minDur*100).."%"
end
local AutoRepair = function()
if not RepairBrokerDB.autoRepair then return end
local cost, canRepair = GetRepairAllCost()
if not canRepair or cost == 0 then return end
-- Use guildbank to repair
if CanWithdrawGuildBankMoney() and RepairBrokerDB.useBuildBank and GetGuildBankMoney() >= cost then
RepairAllItems(1)
elseif GetMoney() >= cost then -- Repair the old fashion way
RepairAllItems()
print("Repaired for "..CopperToString(cost))
else
print("Unable to AutoRepair, you need "..CopperToString(cost - GetMoney()).." more.")
end
end
local OnEvent = function(_, event, ...)
if event ~= "MERCHANT_SHOW" then
UpdateDurability()
else
AutoRepair()
end
end
local event = CreateFrame("Frame")
event:RegisterEvent("ADDON_LOADED")
event:SetScript("OnEvent", function(_, _, addon)
if addon ~= name then return end
OnLoad()
event:SetScript("OnEvent", OnEvent)
event:UnregisterEvent("ADDON_LOADED")
event:RegisterEvent("PLAYER_DEAD")
event:RegisterEvent("PLAYER_UNGHOST")
event:RegisterEvent("PLAYER_REGEN_ENABLED")
event:RegisterEvent("UPDATE_INVENTORY_ALERTS")
event:RegisterEvent("MERCHANT_SHOW")
event:RegisterEvent("MERCHANT_CLOSED")
end)
---------------------------------
-- TOOLTIP
---------------------------------
local tooltip = nil
function Repair:OnEnter()
if tooltip then Repair:OnLeave() end
UpdateDurability()
tooltip = LibTooltip:Acquire("RepairTooltip", 3, "LEFT", "CENTER", "RIGHT")
tooltip:AddHeader("Equipted items")
local dur, totalCost, cost = 0, 0, nil
local gray = "|cFFAAAAAA"
for i,info in ipairs(slots) do
dur = math.floor(info[3]*100)
if dur >= 0 then
dur = DurabilityColor(info[3])..dur
dur = dur.."%"
else
dur = DurabilityColor(-1).."- "
end
cost = select(3, GameTooltip:SetInventoryItem("player", info[1]))
tooltip:AddLine(
gray..info[2], -- Slot
dur, -- Dur
CopperToString(cost) -- Cost
)
if cost and cost > 0 then totalCost = totalCost + cost end
end
local cost, dur, maxDur = 0, 1, 1
for bag = 0, 4 do
for slot = 1, GetContainerNumSlots(bag) do
-- Cost
local _, repairCost = GameTooltip:SetBagItem(bag, slot)
if repairCost then cost = cost + repairCost end
-- Dur
d, m = GetContainerItemDurability(bag, slot)
if d and m then dur = dur + d; maxDur = maxDur + m end
end
end
GameTooltip:Hide()
local averageDur = dur/maxDur
tooltip:AddHeader(" ")
tooltip:AddHeader("Inventory")
tooltip:AddLine(
gray.."Items in your bags", -- Slot
DurabilityColor(averageDur)..math.floor(100*averageDur).."%", -- Dur
CopperToString(cost) -- Cost
)
if cost and cost > 0 then totalCost = totalCost + cost end
if totalCost > 0 then
tooltip:AddHeader(" ")
tooltip:AddHeader("Total cost")
local m = 1
for i=4, 8 do
tooltip:AddLine(
gray.._G["FACTION_STANDING_LABEL"..i], -- Slot
" ", -- Dur
CopperToString(math.floor(totalCost*m+.5)) -- Cost
)
m = m - .05
end
end
tooltip:SmartAnchorTo(self)
tooltip:Show()
end
function Repair:OnLeave()
if not tooltip then return end
tooltip:Clear()
LibTooltip:Release(tooltip)
tooltip = nil
end

13
RepairBroker.toc Normal file
View File

@ -0,0 +1,13 @@
## Interface: 30000
## Title: Repair Broker
## Notes: Auto repair + shows durability.
## Author: Merl @ chainwet.net
## Version: @project-version@
## DefaultState: enabled
## SavedVariables: RepairBrokerDB
libs\LibStub\LibStub.lua
libs\CallbackHandler-1.0\CallbackHandler-1.0.lua
libs\LibDataBroker-1.1\LibDataBroker-1.1.lua
libs\LibTooltip-1.0\LibTooltip-1.0.lua
RepairBroker.lua

View File

@ -0,0 +1,239 @@
--[[ $Id: CallbackHandler-1.0.lua 60697 2008-02-09 16:51:20Z nevcairiel $ ]]
local MAJOR, MINOR = "CallbackHandler-1.0", 3
local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR)
if not CallbackHandler then return end -- No upgrade needed
local meta = {__index = function(tbl, key) tbl[key] = {} return tbl[key] end}
local type = type
local pcall = pcall
local pairs = pairs
local assert = assert
local concat = table.concat
local loadstring = loadstring
local next = next
local select = select
local type = type
local xpcall = xpcall
local function errorhandler(err)
return geterrorhandler()(err)
end
local function CreateDispatcher(argCount)
local code = [[
local next, xpcall, eh = ...
local method, ARGS
local function call() method(ARGS) end
local function dispatch(handlers, ...)
local index
index, method = next(handlers)
if not method then return end
local OLD_ARGS = ARGS
ARGS = ...
repeat
xpcall(call, eh)
index, method = next(handlers, index)
until not method
ARGS = OLD_ARGS
end
return dispatch
]]
local ARGS, OLD_ARGS = {}, {}
for i = 1, argCount do ARGS[i], OLD_ARGS[i] = "arg"..i, "old_arg"..i end
code = code:gsub("OLD_ARGS", concat(OLD_ARGS, ", ")):gsub("ARGS", concat(ARGS, ", "))
return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(next, xpcall, errorhandler)
end
local Dispatchers = setmetatable({}, {__index=function(self, argCount)
local dispatcher = CreateDispatcher(argCount)
rawset(self, argCount, dispatcher)
return dispatcher
end})
--------------------------------------------------------------------------
-- CallbackHandler:New
--
-- target - target object to embed public APIs in
-- RegisterName - name of the callback registration API, default "RegisterCallback"
-- UnregisterName - name of the callback unregistration API, default "UnregisterCallback"
-- UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API.
function CallbackHandler:New(target, RegisterName, UnregisterName, UnregisterAllName, OnUsed, OnUnused)
-- TODO: Remove this after beta has gone out
assert(not OnUsed and not OnUnused, "ACE-80: OnUsed/OnUnused are deprecated. Callbacks are now done to registry.OnUsed and registry.OnUnused")
RegisterName = RegisterName or "RegisterCallback"
UnregisterName = UnregisterName or "UnregisterCallback"
if UnregisterAllName==nil then -- false is used to indicate "don't want this method"
UnregisterAllName = "UnregisterAllCallbacks"
end
-- we declare all objects and exported APIs inside this closure to quickly gain access
-- to e.g. function names, the "target" parameter, etc
-- Create the registry object
local events = setmetatable({}, meta)
local registry = { recurse=0, events=events }
-- registry:Fire() - fires the given event/message into the registry
function registry:Fire(eventname, ...)
if not rawget(events, eventname) or not next(events[eventname]) then return end
local oldrecurse = registry.recurse
registry.recurse = oldrecurse + 1
Dispatchers[select('#', ...) + 1](events[eventname], eventname, ...)
registry.recurse = oldrecurse
if registry.insertQueue and oldrecurse==0 then
-- Something in one of our callbacks wanted to register more callbacks; they got queued
for eventname,callbacks in pairs(registry.insertQueue) do
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
for self,func in pairs(callbacks) do
events[eventname][self] = func
-- fire OnUsed callback?
if first and registry.OnUsed then
registry.OnUsed(registry, target, eventname)
first = nil
end
end
end
registry.insertQueue = nil
end
end
-- Registration of a callback, handles:
-- self["method"], leads to self["method"](self, ...)
-- self with function ref, leads to functionref(...)
-- "addonId" (instead of self) with function ref, leads to functionref(...)
-- all with an optional arg, which, if present, gets passed as first argument (after self if present)
target[RegisterName] = function(self, eventname, method, ... --[[actually just a single arg]])
if type(eventname) ~= "string" then
error("Usage: "..RegisterName.."(eventname, method[, arg]): 'eventname' - string expected.", 2)
end
method = method or eventname
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
if type(method) ~= "string" and type(method) ~= "function" then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - string or function expected.", 2)
end
local regfunc
if type(method) == "string" then
-- self["method"] calling style
if type(self) ~= "table" then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): self was not a table?", 2)
elseif self==target then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): do not use Library:"..RegisterName.."(), use your own 'self'", 2)
elseif type(self[method]) ~= "function" then
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - method '"..tostring(method).."' not found on self.", 2)
end
if select("#",...)>=1 then -- this is not the same as testing for arg==nil!
local arg=select(1,...)
regfunc = function(...) self[method](self,arg,...) end
else
regfunc = function(...) self[method](self,...) end
end
else
-- function ref with self=object or self="addonId"
if type(self)~="table" and type(self)~="string" then
error("Usage: "..RegisterName.."(self or \"addonId\", eventname, method): 'self or addonId': table or string expected.", 2)
end
if select("#",...)>=1 then -- this is not the same as testing for arg==nil!
local arg=select(1,...)
regfunc = function(...) method(arg,...) end
else
regfunc = method
end
end
if events[eventname][self] or registry.recurse<1 then
-- if registry.recurse<1 then
-- we're overwriting an existing entry, or not currently recursing. just set it.
events[eventname][self] = regfunc
-- fire OnUsed callback?
if registry.OnUsed and first then
registry.OnUsed(registry, target, eventname)
end
else
-- we're currently processing a callback in this registry, so delay the registration of this new entry!
-- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency
registry.insertQueue = registry.insertQueue or setmetatable({},meta)
registry.insertQueue[eventname][self] = regfunc
end
end
-- Unregister a callback
target[UnregisterName] = function(self, eventname)
if not self or self==target then
error("Usage: "..UnregisterName.."(eventname): bad 'self'", 2)
end
if type(eventname) ~= "string" then
error("Usage: "..UnregisterName.."(eventname): 'eventname' - string expected.", 2)
end
if rawget(events, eventname) and events[eventname][self] then
events[eventname][self] = nil
-- Fire OnUnused callback?
if registry.OnUnused and not next(events[eventname]) then
registry.OnUnused(registry, target, eventname)
end
end
if registry.insertQueue and rawget(registry.insertQueue, eventname) and registry.insertQueue[eventname][self] then
registry.insertQueue[eventname][self] = nil
end
end
-- OPTIONAL: Unregister all callbacks for given selfs/addonIds
if UnregisterAllName then
target[UnregisterAllName] = function(...)
if select("#",...)<1 then
error("Usage: "..UnregisterAllName.."([whatFor]): missing 'self' or \"addonId\" to unregister events for.", 2)
end
if select("#",...)==1 and ...==target then
error("Usage: "..UnregisterAllName.."([whatFor]): supply a meaningful 'self' or \"addonId\"", 2)
end
for i=1,select("#",...) do
local self = select(i,...)
if registry.insertQueue then
for eventname, callbacks in pairs(registry.insertQueue) do
if callbacks[self] then
callbacks[self] = nil
end
end
end
for eventname, callbacks in pairs(events) do
if callbacks[self] then
callbacks[self] = nil
-- Fire OnUnused callback?
if registry.OnUnused and not next(callbacks) then
registry.OnUnused(registry, target, eventname)
end
end
end
end
end
end
return registry
end
-- CallbackHandler purposefully does NOT do explicit embedding. Nor does it
-- try to upgrade old implicit embeds since the system is selfcontained and
-- relies on closures to work.

View File

@ -0,0 +1,4 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="CallbackHandler-1.0.lua"/>
</Ui>

View File

@ -0,0 +1,66 @@
assert(LibStub, "LibDataBroker-1.1 requires LibStub")
assert(LibStub:GetLibrary("CallbackHandler-1.0", true), "LibDataBroker-1.1 requires CallbackHandler-1.0")
local lib, oldminor = LibStub:NewLibrary("LibDataBroker-1.1", 3)
if not lib then return end
oldminor = oldminor or 0
lib.callbacks = lib.callbacks or LibStub:GetLibrary("CallbackHandler-1.0"):New(lib)
lib.attributestorage, lib.namestorage, lib.proxystorage = lib.attributestorage or {}, lib.namestorage or {}, lib.proxystorage or {}
local attributestorage, namestorage, callbacks = lib.attributestorage, lib.namestorage, lib.callbacks
if oldminor < 2 then
lib.domt = {
__metatable = "access denied",
__index = function(self, key) return attributestorage[self] and attributestorage[self][key] end,
}
end
if oldminor < 3 then
lib.domt.__newindex = function(self, key, value)
if not attributestorage[self] then attributestorage[self] = {} end
if attributestorage[self][key] == value then return end
attributestorage[self][key] = value
local name = namestorage[self]
if not name then return end
callbacks:Fire("LibDataBroker_AttributeChanged", name, key, value, self)
callbacks:Fire("LibDataBroker_AttributeChanged_"..name, name, key, value, self)
callbacks:Fire("LibDataBroker_AttributeChanged_"..name.."_"..key, name, key, value, self)
callbacks:Fire("LibDataBroker_AttributeChanged__"..key, name, key, value, self)
end
end
if oldminor < 2 then
function lib:NewDataObject(name, dataobj)
if self.proxystorage[name] then return end
if dataobj then
assert(type(dataobj) == "table", "Invalid dataobj, must be nil or a table")
self.attributestorage[dataobj] = {}
for i,v in pairs(dataobj) do
self.attributestorage[dataobj][i] = v
dataobj[i] = nil
end
end
dataobj = setmetatable(dataobj or {}, self.domt)
self.proxystorage[name], self.namestorage[dataobj] = dataobj, name
self.callbacks:Fire("LibDataBroker_DataObjectCreated", name, dataobj)
return dataobj
end
end
if oldminor < 1 then
function lib:DataObjectIterator()
return pairs(self.proxystorage)
end
function lib:GetDataObjectByName(dataobjectname)
return self.proxystorage[dataobjectname]
end
function lib:GetNameByDataObject(dataobject)
return self.namestorage[dataobject]
end
end

30
libs/LibStub/LibStub.lua Normal file
View File

@ -0,0 +1,30 @@
-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info
-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
local LibStub = _G[LIBSTUB_MAJOR]
if not LibStub or LibStub.minor < LIBSTUB_MINOR then
LibStub = LibStub or {libs = {}, minors = {} }
_G[LIBSTUB_MAJOR] = LibStub
LibStub.minor = LIBSTUB_MINOR
function LibStub:NewLibrary(major, minor)
assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)")
minor = assert(tonumber(strmatch(minor, "%d+")), "Minor version must either be a number or contain a number.")
local oldminor = self.minors[major]
if oldminor and oldminor >= minor then return nil end
self.minors[major], self.libs[major] = minor, self.libs[major] or {}
return self.libs[major], oldminor
end
function LibStub:GetLibrary(major, silent)
if not self.libs[major] and not silent then
error(("Cannot find a library instance of %q."):format(tostring(major)), 2)
end
return self.libs[major], self.minors[major]
end
function LibStub:IterateLibraries() return pairs(self.libs) end
setmetatable(LibStub, { __call = LibStub.GetLibrary })
end

9
libs/LibStub/LibStub.toc Normal file
View File

@ -0,0 +1,9 @@
## Interface: 20100
## Title: Lib: LibStub
## Notes: Universal Library Stub
## Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel
## X-Website: http://jira.wowace.com/browse/LS | http://www.wowace.com/wiki/LibStub
## X-Category: Library
## X-License: Public Domain
LibStub.lua

View File

@ -0,0 +1,616 @@
--[[
Copyright (c) 2008, LibTooltip Development Team
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Redistribution of a stand alone version is strictly prohibited without
prior written authorization from the Lead of the LibTooltip Development Team.
* Neither the name of the LibTooltip Development Team nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--]]
assert(LibStub, "LibTooltip-1.0 requires LibStub")
local MAJOR, MINOR = "LibTooltip-1.0", 2
local LibTooltip, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
if not LibTooltip then return end -- No upgrade needed
-- Internal constants to tweak the layout
local TOOLTIP_PADDING = 10
local CELL_MARGIN = 3
local bgFrame = {
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
tile = true,
tileSize = 16,
edgeSize = 16,
insets = {left = 5, right = 5, top = 5, bottom = 5}
}
------------------------------------------------------------------------------
-- Tables and locals
------------------------------------------------------------------------------
LibTooltip.frameMetatable = LibTooltip.frameMetatable or {__index = CreateFrame("Frame")}
LibTooltip.tipPrototype = LibTooltip.tipPrototype or setmetatable({}, LibTooltip.frameMetatable)
LibTooltip.tipMetatable = LibTooltip.tipMetatable or {__index = LibTooltip.tipPrototype}
LibTooltip.providerPrototype = LibTooltip.providerPrototype or {}
LibTooltip.providerMetatable = LibTooltip.providerMetatable or {__index = LibTooltip.providerPrototype}
LibTooltip.cellPrototype = LibTooltip.cellPrototype or setmetatable({}, LibTooltip.frameMetatable)
LibTooltip.cellMetatable = LibTooltip.cellMetatable or { __index = LibTooltip.cellPrototype }
LibTooltip.activeTooltips = LibTooltip.activeTooltips or {}
LibTooltip.tooltipHeap = LibTooltip.tooltipHeap or {}
LibTooltip.frameHeap = LibTooltip.frameHeap or {}
local tipPrototype = LibTooltip.tipPrototype
local tipMetatable = LibTooltip.tipMetatable
local providerPrototype = LibTooltip.providerPrototype
local providerMetatable = LibTooltip.providerMetatable
local cellPrototype = LibTooltip.cellPrototype
local cellMetatable = LibTooltip.cellMetatable
local activeTooltips = LibTooltip.activeTooltips
local tooltipHeap = LibTooltip.tooltipHeap
local frameHeap = LibTooltip.frameHeap
-- Tooltip private methods
local InitializeTooltip, FinalizeTooltip, ResetTooltipSize, ResizeColspans
local AcquireCell, ReleaseCell
------------------------------------------------------------------------------
-- Public library API
------------------------------------------------------------------------------
function LibTooltip:Acquire(key, ...)
if key == nil then
error("attempt to use a nil key", 2)
end
local tooltip = activeTooltips[key]
if not tooltip then
tooltip = tremove(tooltipHeap) or setmetatable(CreateFrame("Frame", nil, UIParent), tipMetatable)
InitializeTooltip(tooltip, key)
activeTooltips[key] = tooltip
end
if select('#', ...) > 0 then
-- Here we catch any error to properly report it for the calling code
local ok, msg = pcall(tooltip.SetColumnLayout, tooltip, ...)
if not ok then error(msg, 2) end
end
return tooltip
end
function LibTooltip:IsAcquired(key)
if key == nil then
error("attempt to use a nil key", 2)
end
return not not activeTooltips[key]
end
function LibTooltip:Release(tooltip)
local key = tooltip and tooltip.key
if not key or activeTooltips[key] ~= tooltip then return end
tooltip:Hide()
FinalizeTooltip(tooltip)
tinsert(tooltipHeap, tooltip)
activeTooltips[key] = nil
end
function LibTooltip:IterateTooltips()
return pairs(activeTooltips)
end
------------------------------------------------------------------------------
-- Frame heap
------------------------------------------------------------------------------
local function AcquireFrame(parent)
local frame = tremove(frameHeap) or CreateFrame("Frame")
frame:SetParent(parent)
return frame
end
local function ReleaseFrame(frame)
frame:Hide()
frame:SetParent(nil)
frame:ClearAllPoints()
tinsert(frameHeap, frame)
end
------------------------------------------------------------------------------
-- CellProvider and Cell
------------------------------------------------------------------------------
-- Provider prototype
function providerPrototype:AcquireCell(tooltip)
local cell = tremove(self.heap)
if not cell then
cell = setmetatable(CreateFrame("Frame", nil, tooltip), self.cellMetatable)
if type(cell.InitializeCell) == 'function' then
cell:InitializeCell()
end
end
self.cells[cell] = true
return cell
end
function providerPrototype:ReleaseCell(cell)
if not self.cells[cell] then return end
if type(cell.ReleaseCell) == 'function' then
cell:ReleaseCell()
end
self.cells[cell] = nil
tinsert(self.heap, cell)
end
function providerPrototype:GetCellPrototype()
return self.cellPrototype, self.cellMetatable
end
function providerPrototype:IterateCells()
return pairs(self.cells)
end
-- Cell provider factory
function LibTooltip:CreateCellProvider(baseProvider)
local cellBaseMetatable, cellBasePrototype
if baseProvider and baseProvider.GetCellPrototype then
cellBasePrototype, cellBaseMetatable = baseProvider:GetCellPrototype()
else
cellBaseMetatable = cellMetatable
end
local cellPrototype = setmetatable({}, cellBaseMetatable)
local cellProvider = setmetatable({}, providerMetatable)
cellProvider.heap = {}
cellProvider.cells = {}
cellProvider.cellPrototype = cellPrototype
cellProvider.cellMetatable = { __index = cellPrototype }
return cellProvider, cellPrototype, cellBasePrototype
end
------------------------------------------------------------------------------
-- Basic label provider
------------------------------------------------------------------------------
if not LibTooltip.LabelProvider then
LibTooltip.LabelProvider, LibTooltip.LabelPrototype = LibTooltip:CreateCellProvider()
end
local labelProvider = LibTooltip.LabelProvider
local labelPrototype = LibTooltip.LabelPrototype
function labelPrototype:InitializeCell()
self.fontString = self:CreateFontString()
self.fontString:SetAllPoints(self)
self.fontString:SetFontObject(GameTooltipText)
end
function labelPrototype:SetupCell(tooltip, value, justification, font, ...)
local fs = self.fontString
fs:SetFontObject(font or tooltip:GetFont())
fs:SetJustifyH(justification)
fs:SetText(tostring(value))
fs:Show()
return fs:GetStringWidth(), fs:GetStringHeight()
end
------------------------------------------------------------------------------
-- Helpers
------------------------------------------------------------------------------
local function checkFont(font, level, silent)
if not font or type(font) ~= 'table' or type(font.IsObjectType) ~= 'function' or not font:IsObjectType("Font") then
if silent then
return false
else
error("font must be Font instance, not: "..tostring(font), level+1)
end
end
return true
end
local function checkJustification(justification, level, silent)
if justification ~= "LEFT" and justification ~= "CENTER" and justification ~= "RIGHT" then
if silent then
return false
else
error("invalid justification, must one of LEFT, CENTER or RIGHT, not: "..tostring(justification), level+1)
end
end
return true
end
------------------------------------------------------------------------------
-- Tooltip prototype
------------------------------------------------------------------------------
function InitializeTooltip(self, key)
-- (Re)set frame settings
self:SetBackdrop(bgFrame)
self:SetBackdropColor(0.09, 0.09, 0.09)
self:SetBackdropBorderColor(1, 1, 1)
self:SetAlpha(0.9)
self:SetScale(1.0)
self:SetFrameStrata("TOOLTIP")
self:SetClampedToScreen(false)
-- Our data
self.key = key
self.columns = self.columns or {}
self.lines = self.lines or {}
self.colspans = self.colspans or {}
self.regularFont = GameTooltipText
self.headerFont = GameTooltipHeaderText
self:SetScript('OnShow', ResizeColspans)
ResetTooltipSize(self)
end
function tipPrototype:SetColumnLayout(numColumns, ...)
if type(numColumns) ~= "number" or numColumns < 1 then
error("number of columns must be a positive number, not: "..tostring(numColumns), 2)
end
for i = 1, numColumns do
local justification = select(i, ...) or "LEFT"
checkJustification(justification, 2)
if self.columns[i] then
self.columns[i].justification = justification
else
self:AddColumn(justification)
end
end
end
function tipPrototype:AddColumn(justification)
justification = justification or "LEFT"
checkJustification(justification, 2)
local colNum = #self.columns + 1
local column = AcquireFrame(self)
column.justification = justification
column.width = 0
column:SetWidth(0)
column:SetPoint("TOP", self, "TOP", 0, -TOOLTIP_PADDING)
column:SetPoint("BOTTOM", self, "BOTTOM", 0, TOOLTIP_PADDING)
if colNum > 1 then
column:SetPoint("LEFT", self.columns[colNum-1], "RIGHT", CELL_MARGIN, 0)
self.width = self.width + CELL_MARGIN
self:SetWidth(self.width)
else
column:SetPoint("LEFT", self, "LEFT", TOOLTIP_PADDING, 0)
end
column:Show()
self.columns[colNum] = column
return colNum
end
function FinalizeTooltip(self)
self:Clear()
for i, column in ipairs(self.columns) do
column:Hide()
ReleaseFrame(column)
self.columns[i] = nil
end
end
function ResetTooltipSize(self)
self.width = 2*TOOLTIP_PADDING + math.max(0, CELL_MARGIN * (#self.columns - 1))
self.height = 2*TOOLTIP_PADDING
self:SetWidth(self.width)
self:SetHeight(self.height)
end
function tipPrototype:Clear()
for i, line in ipairs(self.lines) do
for j, cell in ipairs(line.cells) do
ReleaseCell(self, cell)
line.cells[j] = nil
end
line:Hide()
ReleaseFrame(line)
self.lines[i] = nil
end
for i, column in ipairs(self.columns) do
column.width = 0
column:SetWidth(0)
end
for k in pairs(self.colspans) do
self.colspans[k] = nil
end
ResetTooltipSize(self)
end
function tipPrototype:SetFont(font)
checkFont(font, 2)
self.regularFont = font
end
function tipPrototype:GetFont() return self.regularFont end
function tipPrototype:SetHeaderFont(font)
checkFont(font, 2)
self.headerFont = font
end
function tipPrototype:GetHeaderFont() return self.headerFont end
local function EnlargeColumn(self, column, width)
if width > column.width then
self.width = self.width + width - column.width
self:SetWidth(self.width)
column.width = width
column:SetWidth(width)
end
end
function ResizeColspans(self)
if not self:IsShown() then return end
local columns = self.columns
for colRange, width in pairs(self.colspans) do
local left, right = colRange:match("(%d)%-(%d)")
left, right = tonumber(left), tonumber(right)
for col = left, right-1 do
width = width - columns[col].width - CELL_MARGIN
end
EnlargeColumn(self, columns[right], width)
self.colspans[colRange] = nil
end
end
function AcquireCell(self, provider)
local cell = provider:AcquireCell(self)
cell:SetParent(self)
cell:SetFrameLevel(self:GetFrameLevel()+1)
return cell
end
function ReleaseCell(self, cell)
if cell and cell:GetParent() == self then
cell:Hide()
cell:SetParent(nil)
cell:ClearAllPoints()
cell._provider:ReleaseCell(cell)
end
end
local function _SetCell(self, lineNum, colNum, value, font, justification, colSpan, provider, ...)
local line = self.lines[lineNum]
local cells = line.cells
-- Unset: be quick
if value == nil then
local cell = cells[colNum]
if cell then
for i = colNum, colNum + cell._colSpan - 1 do
cells[i] = nil
end
ReleaseCell(self, cell)
end
return lineNum, colNum
end
-- Check previous cell
local cell
local prevCell = cells[colNum]
if prevCell == false then
error("overlapping cells at column "..colNum, 3)
elseif prevCell then
-- There is a cell here
font = font or prevCell._font
justification = justification or prevCell._justification
colSpan = colSpan or prevCell._colSpan
if provider == nil or prevCell._provider == provider then
-- Reuse existing cell
cell = prevCell
provider = cell._provider
else
-- A new cell is required
ReleaseCell(self, prevCell)
cells[colNum] = nil
end
else
-- Creating a new cell, use meaning full defaults
provider = provider or labelProvider
font = font or self.regularFont
justification = justification or self.columns[colNum].justification or "LEFT"
colSpan = colSpan or 1
end
local tooltipWidth = #self.columns
local rightColNum
if colSpan > 0 then
rightColNum = colNum + colSpan - 1
if rightColNum > tooltipWidth then
error("ColSpan too big, cell extends beyond right-most column", 3)
end
else
-- Zero or negative: count back from right-most columns
rightColNum = math.max(colNum, tooltipWidth + colSpan)
-- Update colspan to its effective value
colSpan = 1 + rightColNum - colNum
end
-- Cleanup colspans
for i = colNum + 1, rightColNum do
local cell = cells[i]
if cell == false then
error("overlapping cells at column "..i, 3)
elseif cell then
ReleaseCell(self, cell)
end
cells[i] = false
end
-- Create the cell and anchor it
if not cell then
cell = AcquireCell(self, provider)
cell:SetPoint("LEFT", self.columns[colNum], "LEFT", 0, 0)
cell:SetPoint("TOP", line, "TOP", 0, 0)
cell:SetPoint("BOTTOM", line, "BOTTOM", 0, 0)
cells[colNum] = cell
end
cell:SetPoint("RIGHT", self.columns[rightColNum], "RIGHT", 0, 0)
-- Store the cell settings directly into the cell
-- That's a bit risky but is is really cheap compared to other ways to do it
cell._provider, cell._font, cell._justification, cell._colSpan = provider, font, justification, colSpan
-- Setup the cell content
local width, height = cell:SetupCell(tooltip, value, justification, font, ...)
-- Enforce cell size
cell:SetWidth(width)
cell:SetHeight(height)
cell:Show()
if colSpan > 1 then
-- Postpone width changes until the tooltip is shown
local colRange = colNum.."-"..rightColNum
self.colspans[colRange] = math.max(self.colspans[colRange] or 0, width)
else
-- Enlarge the column and tooltip if need be
EnlargeColumn(self, self.columns[colNum], width)
end
-- Enlarge the line and tooltip if need be
if height > line.height then
self.height = self.height + height - line.height
self:SetHeight(self.height)
line.height = height
line:SetHeight(height)
end
if rightColNum < tooltipWidth then
return lineNum, rightColNum+1
else
return lineNum, nil
end
end
local function CreateLine(self, font, ...)
local line = AcquireFrame(self)
local lineNum = #self.lines + 1
line:SetPoint('LEFT', self, 'LEFT', TOOLTIP_PADDING, 0)
line:SetPoint('RIGHT', self, 'RIGHT', -TOOLTIP_PADDING, 0)
if lineNum > 1 then
line:SetPoint('TOP', self.lines[lineNum-1], 'BOTTOM', 0, -CELL_MARGIN)
self.height = self.height + CELL_MARGIN
self:SetHeight(self.height)
else
line:SetPoint('TOP', self, 'TOP', 0, -TOOLTIP_PADDING)
end
self.lines[lineNum] = line
line.cells = line.cells or {}
line.height = 0
line:SetHeight(0)
line:Show()
local colNum = 1
for i = 1, #self.columns do
local value = select(i, ...)
if value ~= nil then
lineNum, colNum = _SetCell(self, lineNum, i, value, font, nil, 1, labelProvider)
end
end
return lineNum, colNum
end
function tipPrototype:AddLine(...)
local lineNum, colNum = CreateLine(self, self.regularFont, ...)
ResizeColspans(self)
return lineNum, colNum
end
function tipPrototype:AddHeader(...)
local lineNum, colNum = CreateLine(self, self.headerFont, ...)
ResizeColspans(self)
return lineNum, colNum
end
function tipPrototype:SetCell(lineNum, colNum, value, ...)
-- Mandatory argument checking
if type(lineNum) ~= "number" then
error("line number must be a number, not: "..tostring(lineNum), 2)
elseif lineNum < 1 or lineNum > #self.lines then
error("line number out of range: "..tostring(lineNum), 2)
elseif type(colNum) ~= "number" then
error("column number must be a number, not: "..tostring(colNum), 2)
elseif colNum < 1 or colNum > #self.columns then
error("column number out of range: "..tostring(colNum), 2)
end
-- Variable argument checking
local font, justification, colSpan, provider
local i, arg = 1, ...
if arg == nil or checkFont(arg, 2, true) then
i, font, arg = 2, ...
end
if arg == nil or checkJustification(arg, 2, true) then
i, justification, arg = i+1, select(i, ...)
end
if arg == nil or type(arg) == 'number' then
i, colSpan, arg = i+1, select(i, ...)
end
if arg == nil or type(arg) == 'table' and type(arg.AcquireCell) == 'function' then
i, provider = i+1, arg
end
lineNum, colNum = _SetCell(self, lineNum, colNum, value, font, justification, colSpan, provider, select(i, ...))
ResizeColspans(self)
return lineNum, colNum
end
function tipPrototype:GetLineCount() return #self.lines end
function tipPrototype:GetColumnCount() return #self.columns end
------------------------------------------------------------------------------
-- "Smart" Anchoring (work in progress)
------------------------------------------------------------------------------
local function GetTipAnchor(frame)
local x,y = frame:GetCenter()
if not x or not y then return "TOPLEFT", "BOTTOMLEFT" end
local hhalf = (x > UIParent:GetWidth()*2/3) and "RIGHT" or (x < UIParent:GetWidth()/3) and "LEFT" or ""
local vhalf = (y > UIParent:GetHeight()/2) and "TOP" or "BOTTOM"
return vhalf..hhalf, frame, (vhalf == "TOP" and "BOTTOM" or "TOP")..hhalf
end
function tipPrototype:SmartAnchorTo(frame)
if not frame then
error("Invalid frame provided.", 2)
end
self:ClearAllPoints()
self:SetClampedToScreen(true)
self:SetPoint(GetTipAnchor(frame))
end