Nenue@37: --[[ Nenue@37: -- KrakynTools Nenue@37: -- AddOn prototyping library. Nenue@37: -- Nenue@37: --- Bundles an object into the handler queue, returning the core object, and handlers for debug output and co-routine. Nenue@37: -- Addon name is determined in order of: (string first arg, second arg :GetName(), invoking filename). Nenue@37: -- Once an addon name/object is registered, subsequent calls will return a debugger that reports the file or plugin name. Nenue@37: -- @usage addon, print, wrap = KT.register(name, table) or KT.register(addon) or KT.register(addon, plugin) Nenue@37: -- @param name - name of addon, as found in global varargs Nenue@37: -- @param table - addon table from global varargs Nenue@37: -- Nenue@37: -- @param frame - frame object used by addon Nenue@37: -- @param plugin - string name of plugin or an object table to check for lib handlers Nenue@37: -- Nenue@37: -- Handlers: Nenue@37: -- :init() run immediately after KT sets itself up Nenue@37: -- :profile("Name-TruncatedRealm") called the first time SavedVars data becomes available Nenue@37: -- :variables() called upon variables being available Nenue@37: -- :event(event, ...) replaces the event callback Nenue@37: -- :ui() called by /ui when activating Nenue@37: -- Nenue@37: -- Embedded: Nenue@37: -- NOTES: Nenue@37: -- * `name' is passed as is into CreateFrame, so using nil produce an anonymous frame Nenue@37: -- * `coord' is a 4 or 8 size table unpacked into Texture:SetTexCoords() Nenue@37: -- Nenue@37: -- tab = frame:tab(name, tooltip, texture, coord) Nenue@37: -- produces a serial button that changes display tabs Nenue@37: -- Nenue@37: -- button = frame:button(name, text, tooltip, onClick) Nenue@37: -- produces a button with OnClick script Nenue@37: -- Nenue@37: -- uibutton = frame:uibutton(name, text, tooltip, onClick, texture, coord) Nenue@37: -- produces a header button with desired onClick Nenue@37: -- Nenue@37: -- Nenue@37: ]]-- Nenue@37: Nenue@37: local LIBKT_MAJOR, LIBKT_MINOR = "LibKraken", 2 Nenue@37: local KT = LibStub:NewLibrary(LIBKT_MAJOR, LIBKT_MINOR) Nenue@37: Nenue@37: --GLOBALS: KTErrorFrame, LibKTError, SlashCmdList, SLASH_RL1, SLASH_UI1 Nenue@37: local CreateFrame, debugstack, tostring, select = CreateFrame, debugstack, tostring, select Nenue@37: local max, unpack, tinsert = max, unpack, tinsert Nenue@37: local ipairs, xpcall, next, safecall = ipairs, xpcall, next, safecall Nenue@37: local UI_TOGGLE = false Nenue@37: local db Nenue@37: Nenue@37: Nenue@37: KT.handler = CreateFrame('Frame', 'LibKTHostFrame', UIParent) Nenue@37: KT.addons = {} Nenue@37: KT.initStack = {} Nenue@37: KT.varsStack = {} Nenue@37: local print = DEVIAN_WORKSPACE and function(...) _G.print('LKT', ...) end or function() end Nenue@37: local registeredHandles = {} Nenue@37: local handlers = {} Nenue@37: Nenue@37: Nenue@37: --- /rl Nenue@37: -- ReloadUI shortcut Nenue@37: SLASH_RL1 = "/rl" Nenue@37: SlashCmdList.RL = function () Nenue@37: ReloadUI() Nenue@37: end Nenue@37: Nenue@37: --- /ui Nenue@37: -- Run any addon:ui() methods Nenue@37: SLASH_UI1 = "/ui" Nenue@37: SlashCmdList.UI = function () Nenue@37: if UI_TOGGLE then Nenue@37: UI_TOGGLE = false Nenue@37: else Nenue@37: UI_TOGGLE = true Nenue@37: end Nenue@37: for i, frame in pairs(KT.frames) do Nenue@37: if UI_TOGGLE then Nenue@37: if frame.close then Nenue@37: frame.close() Nenue@37: else Nenue@37: frame:Hide() Nenue@37: end Nenue@37: else Nenue@37: if frame.ui then Nenue@37: frame.ui() Nenue@37: end Nenue@37: frame:Show() Nenue@37: end Nenue@37: end Nenue@37: end Nenue@37: Nenue@37: LibKTError = function(msg) Nenue@37: local dstack = debugstack(2) Nenue@37: :gsub("Interface\\AddOns\\",'') Nenue@37: :gsub("<(.-)>", function(a) return '|cFF00FFFF<'.. a ..'>|r' end) Nenue@37: Nenue@37: Nenue@37: Nenue@37: KTErrorFrame.errmsg:SetText(msg) Nenue@37: KTErrorFrame.debugstack:SetText(dstack) Nenue@37: KTErrorFrame:SetHeight(KTErrorFrame.debugstack:GetStringHeight() + KTErrorFrame.errmsg:GetStringHeight() + 12) Nenue@37: KTErrorFrame:Show() Nenue@37: end Nenue@37: Nenue@37: local debuggers = {} Nenue@37: local pending = {} Nenue@37: local processing = false Nenue@37: local isHandled = false Nenue@37: local nodebug = false Nenue@37: function KT.OnEvent (addon, event, ...) Nenue@37: if processing then Nenue@37: local args = {...} Nenue@37: C_Timer.After(0, function() KT.OnEvent(addon, event, unpack(args)) end) Nenue@37: return Nenue@37: else Nenue@37: Nenue@37: end Nenue@37: --- reset state Nenue@37: processing = true Nenue@37: isHandled = false Nenue@37: nodebug = false Nenue@37: Nenue@37: Nenue@37: if addon.event then Nenue@37: nodebug = addon.event(addon, event, ...) Nenue@37: end Nenue@37: Nenue@37: if addon[event] then Nenue@37: nodebug = addon[event](addon, event, ...) or nodebug Nenue@37: addon.missed = 0 Nenue@37: addon.handled = addon.handled + 1 Nenue@37: isHandled = true Nenue@37: else Nenue@37: addon.firstEvent = false Nenue@37: addon.unhandled = addon.unhandled + 1 Nenue@37: addon.missed = addon.missed + 1 Nenue@37: end Nenue@37: Nenue@37: for i, module in ipairs(addon.modules) do Nenue@37: --print(i, module) Nenue@37: if module[event] then Nenue@37: nodebug = module[event](addon, event, ...) or nodebug Nenue@37: addon.missed = 0 Nenue@37: addon.handled = addon.handled + 1 Nenue@37: isHandled = true Nenue@37: else Nenue@37: addon.firstEvent = false Nenue@37: addon.unhandled = addon.unhandled + 1 Nenue@37: addon.missed = addon.missed + 1 Nenue@37: end Nenue@37: Nenue@37: end Nenue@37: --if nodebug then Nenue@37: processing = false Nenue@37: return Nenue@37: --else Nenue@37: -- KT.UpdateEventStatus(addon, event, ...) Nenue@37: -- processing = false Nenue@37: --end Nenue@37: Nenue@37: end Nenue@37: Nenue@37: KT.UpdateEventStatus = function(addon, event, ...) Nenue@37: if (addon.DEVIAN_PNAME and addon.DEVIAN_PNAME == DEVIAN_PNAME) or ((not addon.DEVIAN_PNAME) and DEVIAN_WORKSPACE) then Nenue@37: print(addon:GetName(), event, ...) Nenue@37: end Nenue@37: Nenue@37: -- debug outputs Nenue@37: if addon.status then Nenue@37: addon.status:SetText(event .. '\n|cFF00FF00' .. addon.handled .. '|r |cFFFF8800' .. addon.missed .. '|r |cFFFF4400' .. addon.unhandled .. '|r') Nenue@37: if isHandled then Nenue@37: addon.status:SetTextColor(0,1,0) Nenue@37: if addon.log then Nenue@37: local logtext = event Nenue@37: for i = 1, select('#',...) do Nenue@37: logtext = logtext .. '\n' .. i .. ':' .. tostring(select(i,...)) Nenue@37: end Nenue@37: addon.log:SetText('|cFFFFFF00last|r\n' .. logtext) Nenue@37: local newWidth = addon.log:GetStringWidth() Nenue@37: Nenue@37: if addon.logfirst then Nenue@37: if not addon.firstEvent then Nenue@37: addon.firstEvent = event Nenue@37: addon.logfirst:SetText('|cFF00FF88first|r\n' .. logtext) Nenue@37: end Nenue@37: Nenue@37: newWidth = newWidth + addon.logfirst:GetStringWidth() Nenue@37: end Nenue@37: if addon.logdiff then Nenue@37: if not event ~= addon.firstEvent then Nenue@37: addon.firstEvent = event Nenue@37: addon.logdiff:SetText('|cFF0088FFdiff|r\n' .. logtext) Nenue@37: end Nenue@37: newWidth = newWidth + addon.logdiff:GetStringWidth() Nenue@37: end Nenue@37: --addon:SetWidth(newWidth) Nenue@37: end Nenue@37: else Nenue@37: addon.status:SetTextColor(1,0,0) Nenue@37: end Nenue@37: end Nenue@37: end Nenue@37: Nenue@37: KT.map = function(addon, addonTable) Nenue@37: setmetatable(addonTable, { Nenue@37: __index = function(_,k) Nenue@37: return addon[k] Nenue@37: end, Nenue@37: __newindex = function(_, k, v) Nenue@37: addon[k] = v Nenue@37: end Nenue@37: }) Nenue@37: end Nenue@37: Nenue@37: local emptyFunc = function() end Nenue@37: Nenue@37: KT.register = function(addon, arg, noGUI) Nenue@37: Nenue@37: local name, handler Nenue@37: if type(addon) == 'string' and type(arg) == 'table' then Nenue@37: -- it's a string, i.e. file vararg was passed Nenue@37: if _G[addon] then Nenue@37: -- check if it's the name of a frame and use that Nenue@37: handler = _G[addon] Nenue@37: else Nenue@37: -- re-arrange Nenue@37: name = addon Nenue@37: handler = arg Nenue@37: end Nenue@37: else Nenue@37: handler = addon Nenue@37: assert(type(handler) == 'table', 'Unable to parse ('..tostring(type(addon))..', '..tostring(type(arg))..')') Nenue@37: end Nenue@37: Nenue@37: Nenue@37: local loadedName Nenue@37: local printName Nenue@37: local isModule Nenue@37: Nenue@37: -- if exists, then second argument is probably a module name Nenue@37: if registeredHandles[handler] then Nenue@37: if type(arg) == 'table' then Nenue@37: tinsert(handler.modules, arg) Nenue@37: else Nenue@37: name = arg or debugstack(2,1,0):match(".+\\(%S+)%.lua") Nenue@37: end Nenue@37: isModule = true Nenue@37: Nenue@37: if not name then Nenue@37: name = debugstack(2,1,0):match(".+\\(%S+)%.lua") Nenue@37: end Nenue@37: local handlerName = handler:GetName() Nenue@37: loadedName = '|cFF00FF88'.. tostring(handlerName) .. '|r:|cFFFFFF00'.. tostring(name) Nenue@37: else Nenue@37: if handler.GetName then Nenue@37: name = handler:GetName() Nenue@37: elseif not name then Nenue@37: name = debugstack(2,1,0):match(".+\\(%S+)%.lua") Nenue@37: end Nenue@37: Nenue@37: loadedName = '|cFF00FFFF'.. tostring(name) .. '|r' Nenue@37: Nenue@37: registeredHandles[handler] = name Nenue@37: if handler.SetScript then Nenue@37: handler:SetScript('OnEvent', KT.OnEvent) Nenue@37: end Nenue@37: handler.unhandled = 0 Nenue@37: handler.missed = 0 Nenue@37: handler.handled = 0 Nenue@37: handler.firstEvent = false Nenue@37: handler.modules = {} Nenue@37: tinsert(KT.initStack, handler) Nenue@37: tinsert(KT.varsStack, handler) Nenue@37: Nenue@37: if handler.GetName and (not noGUI) then Nenue@37: handler.UIPanelAnchor = {'TOPLEFT', handler, 'TOPLEFT', 12, -12 } Nenue@37: handler.UIPanelGrowth = {'TOPLEFT', 'TOPRIGHT', 14, 0} Nenue@37: handler.button = KT.button Nenue@37: handler.uibutton = KT.uibutton Nenue@37: handler.tab = KT.tab Nenue@37: handler.print = KT.print Nenue@37: end Nenue@37: end Nenue@37: Nenue@37: local debugID = isModule and name or handler Nenue@37: local debugFunc = emptyFunc Nenue@37: if (handler.DEVIAN_PNAME and DEVIAN_PNAME == handler.DEVIAN_PNAME) or ((not handler.DEVIAN_PNAME) and DEVIAN_WORKSPACE) then Nenue@37: debuggers[debugID] = debuggers[debugID] or function(...) _G.print(name, ...) end Nenue@37: debugFunc = debuggers[debugID] Nenue@37: end Nenue@37: Nenue@37: print(loadedName) Nenue@37: Nenue@37: return handler, debugFunc, KT.wrap Nenue@37: end Nenue@37: Nenue@37: Nenue@37: Nenue@37: local onEvent = function(self, event, arg1) Nenue@37: if (event == 'ADDON_LOADED' and arg1 ~= 'Blizzard_DebugTools') or event == 'PLAYER_LOGIN' then Nenue@37: -- run any init blocks left in the queue Nenue@37: while #KT.initStack >= 1 do Nenue@37: local addon = tremove(KT.initStack, 1) Nenue@37: print('KT', addon:GetName(), 'init') Nenue@37: if addon.init then Nenue@37: xpcall(addon.init, LibKTError) Nenue@37: for i, module in ipairs(addon.modules) do Nenue@37: if module.init then Nenue@37: xpcall(module.init, LibKTError) Nenue@37: end Nenue@37: end Nenue@37: end Nenue@37: end Nenue@37: Nenue@37: -- run any variables blocks if player variables are ready Nenue@37: if IsLoggedIn() and #KT.varsStack >= 1 then Nenue@37: while #KT.varsStack >= 1 do Nenue@37: local addon = tremove(KT.varsStack, 1) Nenue@37: print(addon:GetName()) Nenue@37: if addon.variables then Nenue@37: xpcall(addon.variables, LibKTError) Nenue@37: for i, module in ipairs(addon.modules) do Nenue@37: if module.variables then Nenue@37: xpcall(module.variables, LibKTError) Nenue@37: end Nenue@37: end Nenue@37: end Nenue@37: end Nenue@37: end Nenue@37: end Nenue@37: end Nenue@37: Nenue@37: KT.print = function(module, ...) Nenue@37: local msg = '|cFF00FFFF'..module:GetName()..'|r:' Nenue@37: for i = 1, select('#', ...) do Nenue@37: msg = msg .. ' ' .. tostring(select(i, ...)) Nenue@37: end Nenue@37: DEFAULT_CHAT_FRAME:AddMessage(msg) Nenue@37: end Nenue@37: Nenue@37: --- Button generators Nenue@37: Nenue@37: local GetButtonTemplate = function(name, parent, template, onClick) Nenue@37: if _G[name] then Nenue@37: return _G[name] Nenue@37: end Nenue@37: Nenue@37: local button = CreateFrame('Button', name, parent, template) Nenue@37: button:RegisterForClicks('AnyUp') Nenue@37: button:SetScript('OnClick', onClick) Nenue@37: return button Nenue@37: end Nenue@37: Nenue@37: local SetButtonAnchor = function(self, collector, anchor, growth) Nenue@37: if self:GetID() == 0 then Nenue@37: self:SetID(#collector) Nenue@37: print('registered TabButton #', self:GetID()) Nenue@37: end Nenue@37: Nenue@37: if self:GetID() == 1 then Nenue@37: self:SetPoint(unpack(anchor)) Nenue@37: else Nenue@37: growth[2] = collector[self:GetID()-1] Nenue@37: self:SetPoint(unpack(growth)) Nenue@37: end Nenue@37: end Nenue@37: Nenue@37: KT.tab = function(self, name, tooltip, texture, coords) Nenue@37: local button = GetButtonTemplate(name, self, 'KTTabButton', self.SelectTab) Nenue@37: button.icon:SetTexture(texture) Nenue@37: button.tooltip = tooltip Nenue@37: button:SetSize(unpack(self.tabSize)) Nenue@37: if coords then Nenue@37: button.icon:SetTexCoord(unpack(coords)) Nenue@37: end Nenue@37: SetButtonAnchor(button, self.tabButtons, self.tabAnchor, self.tabGrowth) Nenue@37: return button Nenue@37: end Nenue@37: Nenue@37: KT.button = function(self, name, text, tooltip, onClick) Nenue@37: local button = GetButtonTemplate(name, self, 'KTButton', onClick) Nenue@37: Nenue@37: button.tooltip = tooltip Nenue@37: button:SetText(text) Nenue@37: button:SetWidth(max(button:GetWidth(), button:GetFontString():GetStringWidth() + 12)) Nenue@37: Nenue@37: SetButtonAnchor(button, self.controls, self.controlsAnchor, self.controlsGrowth) Nenue@37: return button Nenue@37: end Nenue@37: Nenue@37: KT.uibutton = function(self, name, text, tooltip, onClick, texture, coords) Nenue@37: local button = GetButtonTemplate(name, self, 'KTUIPanelButton', onClick) Nenue@37: Nenue@37: button.tooltip = tooltip Nenue@37: button:SetText(text) Nenue@37: Nenue@37: if self.UIPanelIcon then Nenue@37: local w, h, anchor, x, y = unpack(self.UIPanelIcon) Nenue@37: button.icon:SetTexture(texture) Nenue@37: button.icon:SetSize(w, h) Nenue@37: button.icon:ClearAllPoints() Nenue@37: button.icon:SetPoint(anchor, button, anchor, x, y) Nenue@37: end Nenue@37: Nenue@37: if not self.UIPanelSize then Nenue@37: button:SetWidth(button:GetFontString():GetStringWidth() + button.icon:GetWidth()/1.5) Nenue@37: else Nenue@37: button:SetSize(unpack(self.UIPanelSize)) Nenue@37: end Nenue@37: if coords then Nenue@37: button.icon:SetTexCoord(unpack(coords)) Nenue@37: end Nenue@37: SetButtonAnchor(button, self.UIPanels, self.UIPanelAnchor, self.UIPanelGrowth) Nenue@37: return button Nenue@37: end Nenue@37: Nenue@37: --- Co-routine Handler kajigger Nenue@37: do Nenue@37: local tickerQueue = {} Nenue@37: local ticker Nenue@37: local instant = false Nenue@37: KT.tick = function() Nenue@37: Nenue@37: if #tickerQueue == 0 then Nenue@37: ticker:Cancel() Nenue@37: ticker = nil Nenue@37: end Nenue@37: local func = tremove(tickerQueue, 1) Nenue@37: if func then Nenue@37: --print('#', #tickerQueue) Nenue@37: func() Nenue@37: end Nenue@37: end Nenue@37: Nenue@37: KT.wrap = function(f) Nenue@37: if not ticker then Nenue@37: --print('create ticker') Nenue@37: ticker = C_Timer.NewTicker(.001, KT.tick) Nenue@37: end Nenue@37: tinsert(tickerQueue, f) Nenue@37: Nenue@37: return #tickerQueue Nenue@37: Nenue@37: end Nenue@37: end Nenue@37: Nenue@37: KT.handler:RegisterEvent('ADDON_LOADED') Nenue@37: KT.handler:RegisterEvent('PLAYER_LOGIN') Nenue@37: KT.handler:SetScript('OnEvent', onEvent)