From a8f0b28af6fb47d3cf839b5b005156756fd55691 Mon Sep 17 00:00:00 2001 From: Kevin Gilbert Date: Mon, 29 Aug 2016 11:38:41 -0700 Subject: [PATCH 1/3] implement LDB handler for UI --- ArtifactMenu.lua | 143 ++++++++++++++++++------- ArtifactMenu.toc | 15 +-- libs/CallbackHandler-1.0.lua | 238 ++++++++++++++++++++++++++++++++++++++++++ libs/LibDataBroker-1.1.lua | 90 ++++++++++++++++ libs/LibStub.lua | 30 ++++++ 5 files changed, 474 insertions(+), 42 deletions(-) create mode 100644 libs/CallbackHandler-1.0.lua create mode 100644 libs/LibDataBroker-1.1.lua create mode 100644 libs/LibStub.lua diff --git a/ArtifactMenu.lua b/ArtifactMenu.lua index b418bb5..7c4d05c 100644 --- a/ArtifactMenu.lua +++ b/ArtifactMenu.lua @@ -1,36 +1,107 @@ -SLASH_ARTIFACTMENU1, SLASH_ARTIFACTMENU2 = '/artifact', '/am'; -SLASH_ARTIFACTMENU3 = "/artifactmenu"; - -local function openWeaponSockets() - SocketInventoryItem(16); -end - --- unneeded, because artifacts are "conjoined" items -local function openOffhandSockets() - SocketInventoryItem(17); -end - -function SlashCmdList.ARTIFACTMENU(msg, editBox) - -- http://wowprogramming.com/docs/api_types#itemQuality - local QUALITY = {} - QUALITY.POOR = 0; - QUALITY.COMMON = 1; - QUALITY.UNCOMMON = 2; - QUALITY.RARE = 3; - QUALITY.EPIC = 4; - QUALITY.LEGENDARY = 5; - QUALITY.ARTIFACT = 6; - QUALITY.HEIRLOOM = 7; - local equippedWeapon = GetInventoryItemID("player", GetInventorySlotInfo("MainHandSlot")); - if equippedWeapon == nil then - print("You do not have any weapons equipped."); - return - end - name, link, quality, iLvl, reqLvl, class, subclass, maxStack, equipSlot, texture, vendorSellPrice = GetItemInfo(equippedWeapon); - if quality == QUALITY["ARTIFACT"] then - openWeaponSockets(); - else - print("You do not have an Artifact Weapon equipped yet."); - end -end - +SLASH_ARTIFACTMENU1, SLASH_ARTIFACTMENU2 = '/artifact', '/am'; +SLASH_ARTIFACTMENU3 = "/artifactmenu"; + +local ldb = LibStub:GetLibrary("LibDataBroker-1.1"); + +local ArtifectMenuFrame = CreateFrame("Button", "ArtifactMenuFrame", UIParent) +ArtifectMenuFrame:RegisterEvent("ADDON_LOADED") +ArtifectMenuFrame:SetScript("OnEvent", function(self) self:SetupFrame() end) + +function openWeaponSockets() + SocketInventoryItem(16); +end + +-- unneeded, because artifacts are "conjoined" items +local function openOffhandSockets() + SocketInventoryItem(17); +end + +function ArtifactMenuFrame:SetupFrame() + self:UnregisterEvent("ADDON_LOADED") + --Frame + self:SetWidth(32) + self:SetHeight(32) + self:SetPoint("CENTER",32,0) + self:EnableMouse(true) + self:SetMovable(true) + self:RegisterForDrag("LeftButton") + self:RegisterForClicks("AnyUp") + self:SetScript("OnDragStart", function(s) s:StartMoving() end) + self:SetScript("OnDragStop", function(s) s:StopMovingOrSizing() end) + --Set Texture + self.tex = self:CreateTexture(nil, "BACKGROUND") + self.tex:SetAllPoints(self) + self.leftGradiant = self:CreateTexture(nil, "BORDER") + self.leftGradiant:SetWidth(16) + self.leftGradiant:SetHeight(14) + self.leftGradiant:SetPoint("LEFT", 0, -5) + self.leftGradiant:SetTexture(1,0,0,1) + self.leftGradiant:SetGradientAlpha("Horizontal", 0, 0, 0, 0.2, 0, 0, 0, 1) + self.rightGradiant = self:CreateTexture(nil, "BORDER") + self.rightGradiant:SetWidth(16) + self.rightGradiant:SetHeight(14) + self.rightGradiant:SetPoint("RIGHT", 0, -5) + self.rightGradiant:SetTexture(1,0,0,1) + self.rightGradiant:SetGradientAlpha("Horizontal", 0, 0, 0, 1, 0, 0, 0, 0.2) + self.text = self:CreateFontString(nil, "ARTWORK", "GameFontWhite") + self.text:SetJustifyH("CENTER") + self.text:SetText("0") + self.text:SetWidth(40) + self.text:SetHeight(14) + self.text:SetPoint("CENTER", 0, -5) + self.text:SetNonSpaceWrap(false) + self.menu = CreateFrame("Frame","ArtifactMenuFrameMenu", self, "UIDropDownMenuTemplate", 1) + self.menuOnLoad = menuOnLoad + UIDropDownMenu_Initialize(self.menu, self.menuOnLoad, "MENU") + self:SetScript("OnClick", + function(s,button,down) + s:OnClick(button,down) + end) + self.dataBroker = ldb:NewDataObject("ArtifactMenu", { + type = "data source", + text = "", + value = "", + label = "Artifact", + OnClick = function (clickedframe, button, down) + ArtifactMenuFrame:OnClick(button, down, clickedframe) + end + }) +end + +function ArtifactMenuFrame:OnClick(button, down, anchorFrame) + self.menu.button = button + anchorFrame = anchorFrame or self + self.anchorFrame = anchorFrame + if button == "LeftButton" then + openArtifactMenu(); + end +end + +function openArtifactMenu() + -- http://wowprogramming.com/docs/api_types#itemQuality + local QUALITY = {} + QUALITY.POOR = 0; + QUALITY.COMMON = 1; + QUALITY.UNCOMMON = 2; + QUALITY.RARE = 3; + QUALITY.EPIC = 4; + QUALITY.LEGENDARY = 5; + QUALITY.ARTIFACT = 6; + QUALITY.HEIRLOOM = 7; + local equippedWeapon = GetInventoryItemID("player", GetInventorySlotInfo("MainHandSlot")); + if equippedWeapon == nil then + print("You do not have any weapons equipped."); + return + end + name, link, quality, iLvl, reqLvl, class, subclass, maxStack, equipSlot, texture, vendorSellPrice = GetItemInfo(equippedWeapon); + if quality == QUALITY["ARTIFACT"] then + openWeaponSockets(); + else + print("You do not have an Artifact Weapon equipped yet."); + end +end + +function SlashCmdList.ARTIFACTMENU(msg, editBox) + openArtifactMenu(); +end + diff --git a/ArtifactMenu.toc b/ArtifactMenu.toc index 9b719bd..c4c8e32 100644 --- a/ArtifactMenu.toc +++ b/ArtifactMenu.toc @@ -1,6 +1,9 @@ -## Interface: 70000 -## Title: ArtifactMenu -## Author: Kevin Gilbert (kaygil) -## Version: 1.0.1 - -ArtifactMenu.lua +## Interface: 70000 +## Title: ArtifactMenu +## Author: Kevin Gilbert (kaygil) +## Version: 1.0.1 + +libs/LibStub.lua +libs/CallbackHandler-1.0.lua +libs/LibDataBroker-1.1.lua +ArtifactMenu.lua diff --git a/libs/CallbackHandler-1.0.lua b/libs/CallbackHandler-1.0.lua new file mode 100644 index 0000000..fd87e02 --- /dev/null +++ b/libs/CallbackHandler-1.0.lua @@ -0,0 +1,238 @@ +--[[ $Id: CallbackHandler-1.0.lua 18 2014-10-16 02:52:20Z mikk $ ]] +local MAJOR, MINOR = "CallbackHandler-1.0", 6 +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} + +-- Lua APIs +local tconcat = table.concat +local assert, error, loadstring = assert, error, loadstring +local setmetatable, rawset, rawget = setmetatable, rawset, rawget +local next, select, pairs, type, tostring = next, select, pairs, type, tostring + +-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded +-- List them here for Mikk's FindGlobals script +-- GLOBALS: geterrorhandler + +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", tconcat(OLD_ARGS, ", ")):gsub("ARGS", tconcat(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) + + 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" or self=thread + if type(self)~="table" and type(self)~="string" and type(self)~="thread" then + error("Usage: "..RegisterName.."(self or \"addonId\", eventname, method): 'self or addonId': table or string or thread 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. + diff --git a/libs/LibDataBroker-1.1.lua b/libs/LibDataBroker-1.1.lua new file mode 100644 index 0000000..f47c0cd --- /dev/null +++ b/libs/LibDataBroker-1.1.lua @@ -0,0 +1,90 @@ + +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", 4) +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 + +if oldminor < 4 then + local next = pairs(attributestorage) + function lib:pairs(dataobject_or_name) + local t = type(dataobject_or_name) + assert(t == "string" or t == "table", "Usage: ldb:pairs('dataobjectname') or ldb:pairs(dataobject)") + + local dataobj = self.proxystorage[dataobject_or_name] or dataobject_or_name + assert(attributestorage[dataobj], "Data object not found") + + return next, attributestorage[dataobj], nil + end + + local ipairs_iter = ipairs(attributestorage) + function lib:ipairs(dataobject_or_name) + local t = type(dataobject_or_name) + assert(t == "string" or t == "table", "Usage: ldb:ipairs('dataobjectname') or ldb:ipairs(dataobject)") + + local dataobj = self.proxystorage[dataobject_or_name] or dataobject_or_name + assert(attributestorage[dataobj], "Data object not found") + + return ipairs_iter, attributestorage[dataobj], 0 + end +end diff --git a/libs/LibStub.lua b/libs/LibStub.lua new file mode 100644 index 0000000..cfc97de --- /dev/null +++ b/libs/LibStub.lua @@ -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 -- 1.7.9.5 From 09745f8451fec6d2a9d881bec211326d46869462 Mon Sep 17 00:00:00 2001 From: Kevin Gilbert Date: Mon, 29 Aug 2016 11:44:22 -0700 Subject: [PATCH 2/3] add special thanks for /u/IHateMyHandle --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7fd658e..5f78880 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,4 @@ This add-on will simplify all of that to just: * Wowpedia [slash commands reference](http://wow.gamepedia.com/Creating_a_slash_command) * The nice people of [#wowuidev](http://us.battle.net/forums/en/wow/topic/1127130079) for helping me debug, specifically `kd3`, `Nebula`, and `Semlar` * [Farrar-Gilneas](http://us.battle.net/wow/en/character/gilneas/Farrar/simple) for testing this add-on in the *Legion* beta. +* [/u/IHateMyHandle](https://www.reddit.com/u/IHateMyHandle), whose add-on [Incentive Program](https://mods.curse.com/addons/wow/incentive-program) served as my inspiration (and also my foundation). The entire LDB frame panel in ArtifactMenu is largely sourced from Incentive Program and I can't be thankful enough. [Incentive Program](https://mods.curse.com/addons/wow/incentive-program) is a QoL add-on (much like ArtifactMenu) which makes queuing for Call To Arms satchels as easy as possible. Go check it out! -- 1.7.9.5 From e1698a4ca9531a8ab05ce77ac7316076cdf8595a Mon Sep 17 00:00:00 2001 From: Kevin Gilbert Date: Mon, 29 Aug 2016 11:46:28 -0700 Subject: [PATCH 3/3] put full name in LDB frame --- ArtifactMenu.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ArtifactMenu.lua b/ArtifactMenu.lua index 7c4d05c..838e7f8 100644 --- a/ArtifactMenu.lua +++ b/ArtifactMenu.lua @@ -61,7 +61,7 @@ function ArtifactMenuFrame:SetupFrame() type = "data source", text = "", value = "", - label = "Artifact", + label = "ArtifactMenu", OnClick = function (clickedframe, button, down) ArtifactMenuFrame:OnClick(button, down, clickedframe) end -- 1.7.9.5