diff --git a/.pkgmeta b/.pkgmeta new file mode 100644 index 0000000..30aca5f --- /dev/null +++ b/.pkgmeta @@ -0,0 +1,5 @@ +enable-nolib-creation: no +tools-used: + - Ace3 + - LibStub + - CallbackHandler-1.0 diff --git a/BindConfig.lua b/BindConfig.lua index cde555f..ba5e218 100755 --- a/BindConfig.lua +++ b/BindConfig.lua @@ -170,11 +170,7 @@ function CliqueConfig:Spellbook_OnBinding(button, key) icon = texture } - if not succ then - CliqueConfig:SetNotification(err) - else - CliqueConfig:UpdateList() - end + CliqueConfig:UpdateList() end function CliqueConfig:Button_OnClick(button) @@ -212,8 +208,9 @@ function CliqueConfig:Button_OnClick(button) config.page1:Hide() config.page2.bindType = "macro" -- Clear out the entries - config.page2.bindText:SetText("") + config.page2.bindText:SetText(L["No binding set"]) config.page2.editbox:SetText("") + config.page2.button_save:Disable() config.page2:Show() end, notCheckable = true, @@ -245,12 +242,6 @@ function CliqueConfig:Button_OnClick(button) end end -function CliqueConfig:SetNotification(text) -end - -function CliqueConfig:ClearNotification() -end - local memoizeBindings = setmetatable({}, {__index = function(t, k, v) local binbits = addon:GetBinaryBindingKey(k) rawset(t, k, binbits) @@ -392,6 +383,10 @@ function CliqueConfig:MacroBindingButton_OnClick(button, key) if key then self.page2.key = key self.page2.bindText:SetText(addon:GetBindingKeyComboText(key)) + self.page2.button_save:Enable() + else + self.page2.bindText:SetText(L["No binding set"]) + self.page2.button_save:Disable() end end diff --git a/Clique.lua b/Clique.lua index bbf65b7..1cc8543 100755 --- a/Clique.lua +++ b/Clique.lua @@ -40,7 +40,14 @@ local addonName, addon = ... local L = addon.L function addon:Initialize() - self:InitializeDatabase() + -- Create an AceDB, but it needs to be cleared first + self.db = LibStub("AceDB-3.0"):New("CliqueDB3", self.defaults) + self.db.RegisterCallback(self, "OnNewProfile", "OnNewProfile") + self.db.RegisterCallback(self, "OnProfileChanged", "OnProfileChanged") + + self.settings = self.db.char + self.bindings = self.db.profile.bindings + self.ccframes = {} self.hccframes = {} @@ -120,23 +127,27 @@ function addon:Initialize() end self:EnableBlizzardFrames() - -- Trigger a profile change, updating all attributes - self:ChangeProfile() - -- Register for combat events to ensure we can swap between the two states self:RegisterEvent("PLAYER_REGEN_DISABLED", "EnteringCombat") self:RegisterEvent("PLAYER_REGEN_ENABLED", "LeavingCombat") - self:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED", function() - self:ChangeProfile() - end) + self:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED", "TalentGroupChanged") + -- Handle combat watching so we can change ooc based on party combat status addon:UpdateCombatWatch() + + -- Trigger a 'TalentGroupChanged' so we end up on the right profile + addon:TalentGroupChanged() + addon:UpdateEverything() end function addon:RegisterFrame(button) self.ccframes[button] = true - button:RegisterForClicks("AnyDown") + if self.settings.downclick then + button:RegisterForClicks("AnyDown") + else + button:RegisterForClicks("AnyUp") + end -- Wrap the OnEnter/OnLeave scripts in order to handle keybindings addon.header:WrapScript(button, "OnEnter", addon.header:GetAttribute("setup_onenter")) @@ -159,90 +170,29 @@ function addon:Enable() CliqueSpellTab.tooltip = L["Clique binding configuration"] end --- Leave CliqueDB in place for now, to ease any migration that users might have. --- Instead use CliqueDB2 for the active database and use versioning to move --- forward from this point. The database consists of two sections: --- * settings - used to handle the basic options Clique uses --- * profiles - used for the binding configuration profiles, possibly shared -local current_db_version = 6 -function addon:InitializeDatabase() - local realmKey = GetRealmName() - local charKey = UnitName("player") .. " - " .. realmKey - addon.staticProfileKey = charKey - - local reset = false - if not CliqueDB2 then - reset = true - elseif CliqueDB2.dbversion == 5 then - if not CliqueDB2.settings or not CliqueDB2.settings[charKey] then - reset = true - else - -- Upgrade to add the blacklist table to settings - CliqueDB2.settings[charKey].blacklist = {} - end - CliqueDB2.dbversion = current_db_version - elseif CliqueDB2.dbversion ~= current_db_version then - reset = true - end - - if reset then - CliqueDB2 = { - settings = {}, - bindings = {}, - dbversion = current_db_version, - } - end - - local db = CliqueDB2 - - addon.db = db - if not db.settings[charKey] then - db.settings[charKey] = { - profileKey = charKey, - blacklist = {}, - blizzframes = { - PlayerFrame = true, - PetFrame = true, - TargetFrame = true, - TargetFrameToT = true, - FocusFrame = true, - FocusFrameToT = true, - arena = true, - party = true, - compactraid = true, - compactparty = true, - boss = true, - }, - } - end - - addon.settings = db.settings[charKey] - self:InitializeBindingProfile() +-- A new profile is being created in the db, called 'profile' +function addon:OnNewProfile(event, db, profile) + table.insert(db.profile.bindings, { + key = "BUTTON1", + type = "target", + unit = "mouseover", + sets = { + default = true + }, + }) + table.insert(db.profile.bindings, { + key = "BUTTON2", + type = "menu", + sets = { + default = true + }, + }) + self.bindings = db.profile.bindings end -function addon:InitializeBindingProfile() - local db = CliqueDB2 - if not db.bindings[addon.settings.profileKey] then - db.bindings[addon.settings.profileKey] = { - [1] = { - key = "BUTTON1", - type = "target", - unit = "mouseover", - sets = { - default = true - }, - }, - [2] = { - key = "BUTTON2", - type = "menu", - sets = { - default = true - }, - }, - } - end - - self.bindings = db.bindings[addon.settings.profileKey] +function addon:OnProfileChanged(event, db, newProfile) + self.bindings = db.profile.bindings + self:UpdateEverything() end local function ATTR(prefix, attr, suffix, value) @@ -412,7 +362,6 @@ end function addon:AddBinding(entry) -- TODO: Check to see if the new binding conflicts with an existing binding - -- TODO: Validate the entry to ensure it has the correct arguments, etc. if not entry.sets then @@ -420,9 +369,7 @@ function addon:AddBinding(entry) end table.insert(self.bindings, entry) - self:UpdateAttributes() - return true end @@ -530,38 +477,23 @@ function addon:UpdateGlobalAttributes() globutton:Execute(globutton.setbinds) end -function addon:ChangeProfile(profileName) - -- Clear the current profile - addon:ClearAttributes() - addon:ClearGlobalAttributes() - - -- Check to see if this is a force-create of a new profile - if type(profileName) == "string" and #profileName > 0 then - -- Do nothing - else - -- Determine which profile to set, based on talent group - self.talentGroup = GetActiveTalentGroup() - if self.talentGroup == 1 and self.settings.pri_profileKey then - profileName = self.settings.pri_profileKey - elseif self.talentGroup == 2 and self.settings.sec_profileKey then - profileName = self.settings.sec_profileKey - end +function addon:TalentGroupChanged() + local currentProfile = self.db:GetCurrentProfile() + local newProfile - if type(profileName) == "string" and addon.db.bindings[profileName] then - -- Do nothing - else - profileName = addon.staticProfileKey - end + -- Determine which profile to set, based on talent group + self.talentGroup = GetActiveTalentGroup() + if self.talentGroup == 1 and self.settings.pri_profileKey then + newProfile = self.settings.pri_profileKey + elseif self.talentGroup == 2 and self.settings.sec_profileKey then + newProfile = self.settings.sec_profileKey end - -- We've been given a profile name, so just change to it - addon.settings.profileKey = profileName - addon:InitializeBindingProfile() - addon:UpdateAttributes() - addon:UpdateGlobalAttributes() - addon:UpdateOptionsPanel() + if newProfile ~= currentProfile and type(newProfile) == "string" then + self.db:SetProfile(newProfile) + end - CliqueConfig:UpdateList() + self:UpdateEverything() end function addon:UpdateCombatWatch() @@ -619,6 +551,14 @@ function addon:CheckPartyCombat(event, unit) end end +function addon:UpdateEverything() + -- Update all running attributes and windows (block) + addon:UpdateAttributes() + addon:UpdateGlobalAttributes() + addon:UpdateOptionsPanel() + CliqueConfig:UpdateList() +end + SLASH_CLIQUE1 = "/clique" SlashCmdList["CLIQUE"] = function(msg, editbox) if SpellBookFrame:IsVisible() then diff --git a/Clique.toc b/Clique.toc index 5a230af..8423325 100755 --- a/Clique.toc +++ b/Clique.toc @@ -3,11 +3,17 @@ ## Author: Cladhaire ## Version: @project-version@ ## Notes: Simply powerful click-casting interface -## SavedVariables: CliqueDB, CliqueDB2 +## SavedVariables: CliqueDB, CliqueDB3 AddonCore.lua Localization.enUS.lua +libs\LibStub\LibStub.lua +libs\CallbackHandler-1.0\CallbackHandler-1.0.xml +libs\AceDB-3.0\AceDB-3.0.xml + +DatabaseDefaults.lua + ClickCastTemplate.xml Clique.xml diff --git a/Clique.xml b/Clique.xml index 4a7c38e..2db5b61 100755 --- a/Clique.xml +++ b/Clique.xml @@ -4,7 +4,7 @@ <Size x="298" y="30"/> <Layers> <Layer level="BORDER"> - <Texture name="$parentIcon" parentKey="icon" file="Interface\Icons\Ability_Druid_Nourish"> + <Texture name="$parentIcon" parentKey="icon" file="Interface\Icons\INV_Misc_QuestionMark"> <Size x="25" y="25"/> <Anchors> <Anchor point="LEFT"> @@ -14,7 +14,7 @@ </Anchor> </Anchors> </Texture> - <FontString name="$parentName" parentKey="name" inherits="GameFontHighlight" justifyH="LEFT" text="Healing Touch"> + <FontString name="$parentName" parentKey="name" inherits="GameFontHighlight" justifyH="LEFT" text="An error has occurred"> <Size> <AbsDimension x="175" y="12"/> </Size> diff --git a/DatabaseDefaults.lua b/DatabaseDefaults.lua new file mode 100644 index 0000000..3eb0917 --- /dev/null +++ b/DatabaseDefaults.lua @@ -0,0 +1,25 @@ +local addonName, addon = ... +local L = addon.L + +addon.defaults = { + char = { + blacklist = {}, + blizzframes = { + PlayerFrame = true, + PetFrame = true, + TargetFrame = true, + TargetFrameToT = true, + FocusFrame = true, + FocusFrameToT = true, + arena = true, + party = true, + compactraid = true, + compactparty = true, + boss = true, + }, + }, + profile = { + bindings = { + }, + }, +} diff --git a/OptionsPanel.lua b/OptionsPanel.lua index 31e4f26..cfa3aa1 100644 --- a/OptionsPanel.lua +++ b/OptionsPanel.lua @@ -62,19 +62,19 @@ function panel:CreateOptions() self.prispeclabel = make_label("CliqueOptionsPriSpecLabel", self, "GameFontNormalSmall") self.prispeclabel:SetText(L["Primary talent spec profile:"]) self.prispec = make_dropdown("CliqueOptionsPriSpec", self) - UIDropDownMenu_SetWidth(self.prispec, 150) + UIDropDownMenu_SetWidth(self.prispec, 200) BlizzardOptionsPanel_SetupDependentControl(self.specswap, self.prispec) self.secspeclabel = make_label("CliqueOptionsSecSpecLabel", self, "GameFontNormalSmall") self.secspeclabel:SetText(L["Secondary talent spec profile:"]) self.secspec = make_dropdown("CliqueOptionsSecSpec", self) - UIDropDownMenu_SetWidth(self.secspec, 150) + UIDropDownMenu_SetWidth(self.secspec, 200) BlizzardOptionsPanel_SetupDependentControl(self.specswap, self.secspec) self.profilelabel = make_label("CliqueOptionsProfileMgmtLabel", self, "GameFontNormalSmall") self.profilelabel:SetText(L["Profile Management:"]) self.profiledd = make_dropdown("CliqueOptionsProfileMgmt", self) - UIDropDownMenu_SetWidth(self.profiledd, 150) + UIDropDownMenu_SetWidth(self.profiledd, 200) -- Collect and anchor the bits together table.insert(bits, self.updown) @@ -125,7 +125,7 @@ StaticPopupDialogs["CLIQUE_NEW_PROFILE"] = { local base = self:GetName() local editbox = getglobal(base .. "EditBox") local profileName = editbox:GetText() - addon:ChangeProfile(profileName) + addon.db:SetProfile(profileName) end, timeout = 0, whileDead = 1, @@ -143,7 +143,7 @@ StaticPopupDialogs["CLIQUE_NEW_PROFILE"] = { local base = self:GetParent():GetName() local editbox = _G[base .. "EditBox"] local profileName = editbox:GetText() - addon:ChangeProfile(profileName) + addon.db:SetProfile(profileName) end self:GetParent():Hide(); end, @@ -163,9 +163,10 @@ StaticPopupDialogs["CLIQUE_NEW_PROFILE"] = { } local function getsorttbl() + local profiles = addon.db:GetProfiles() local sort = {} - for k, v in pairs(addon.db.bindings) do - table.insert(sort, k) + for idx, profileName in ipairs(profiles) do + table.insert(sort, profileName) end table.sort(sort) return sort @@ -224,6 +225,7 @@ end local function mgmt_initialize(dropdown, level) local sort = getsorttbl() local paged = (#sort >= 15) + local currentProfile = addon.db:GetCurrentProfile() if not level or level == 1 then if not paged then @@ -281,24 +283,25 @@ local function mgmt_initialize(dropdown, level) info.text = L["Select profile: %s"]:format(UIDROPDOWNMENU_MENU_VALUE) info.value = sort[UIDROPDOWNMENU_MENU_VALUE] info.notCheckable = true - info.disabled = addon.settings.specswap + -- Don't disable this, allow the user to make their own mistakes + --info.disabled = addon.settings.specswap info.func = function(frame) UIDropDownMenu_SetSelectedValue(dropdown, UIDROPDOWNMENU_MENU_VALUE) UIDropDownMenu_SetText(dropdown, UIDROPDOWNMENU_MENU_VALUE) - addon:ChangeProfile(UIDROPDOWNMENU_MENU_VALUE) + addon.db:SetProfile(UIDROPDOWNMENU_MENU_VALUE) end UIDropDownMenu_AddButton(info, level) info = UIDropDownMenu_CreateInfo() info.text = L["Delete profile: %s"]:format(UIDROPDOWNMENU_MENU_VALUE) - info.disabled = UIDROPDOWNMENU_MENU_VALUE == addon.settings.profileKey + info.disabled = UIDROPDOWNMENU_MENU_VALUE == currentProfile info.value = sort[UIDROPDOWNMENU_MENU_VALUE] info.notCheckable = true info.func = function(frame) local dialog = StaticPopupDialogs["CLIQUE_CONFIRM_PROFILE_DELETE"] dialog.text = L["Delete profile '%s'"]:format(UIDROPDOWNMENU_MENU_VALUE) dialog.OnAccept = function(self) - addon.db.bindings[UIDROPDOWNMENU_MENU_VALUE] = nil + addon.db:DeleteProfile(UIDROPDOWNMENU_MENU_VALUE) end HideDropDownMenu(1) StaticPopup_Show("CLIQUE_CONFIRM_PROFILE_DELETE") @@ -313,19 +316,19 @@ local function mgmt_initialize(dropdown, level) info.func = function(frame) UIDropDownMenu_SetSelectedValue(dropdown, UIDROPDOWNMENU_MENU_VALUE) UIDropDownMenu_SetText(dropdown, UIDROPDOWNMENU_MENU_VALUE) - addon:ChangeProfile(UIDROPDOWNMENU_MENU_VALUE) + addon.db:SetProfile(UIDROPDOWNMENU_MENU_VALUE) end UIDropDownMenu_AddButton(info, level) info = UIDropDownMenu_CreateInfo() info.text = L["Delete profile: %s"]:format(UIDROPDOWNMENU_MENU_VALUE) - info.disabled = UIDROPDOWNMENU_MENU_VALUE == addon.settings.profileKey + info.disabled = UIDROPDOWNMENU_MENU_VALUE == currentProfile info.value = sort[UIDROPDOWNMENU_MENU_VALUE] info.func = function(frame) local dialog = StaticPopupDialogs["CLIQUE_CONFIRM_PROFILE_DELETE"] dialog.text = L["Delete profile '%s'"]:format(UIDROPDOWNMENU_MENU_VALUE) dialog.OnAccept = function(self) - addon.db.bindings[UIDROPDOWNMENU_MENU_VALUE] = nil + addon.db:DeleteProfile(UIDROPDOWNMENU_MENU_VALUE) end HideDropDownMenu(1) StaticPopup_Show("CLIQUE_CONFIRM_PROFILE_DELETE") @@ -338,18 +341,19 @@ end function panel.refresh() -- Initialize the dropdowns local settings = addon.settings - + local currentProfile = addon.db:GetCurrentProfile() + UIDropDownMenu_Initialize(panel.prispec, spec_initialize) - UIDropDownMenu_SetSelectedValue(panel.prispec, settings.pri_profileKey or settings.profileKey) - UIDropDownMenu_SetText(panel.prispec, settings.pri_profileKey or settings.profileKey) + UIDropDownMenu_SetSelectedValue(panel.prispec, settings.pri_profileKey or currentProfile) + UIDropDownMenu_SetText(panel.prispec, settings.pri_profileKey or currentProfile) UIDropDownMenu_Initialize(panel.secspec, spec_initialize) - UIDropDownMenu_SetSelectedValue(panel.secspec, settings.sec_profileKey or settings.profileKey) - UIDropDownMenu_SetText(panel.secspec, settings.sec_profileKey or settings.profileKey) + UIDropDownMenu_SetSelectedValue(panel.secspec, settings.sec_profileKey or currentProfile) + UIDropDownMenu_SetText(panel.secspec, settings.sec_profileKey or currentProfile) UIDropDownMenu_Initialize(panel.profiledd, mgmt_initialize) - UIDropDownMenu_SetSelectedValue(panel.profiledd, settings.profileKey) - UIDropDownMenu_SetText(panel.profiledd, settings.profileKey) + UIDropDownMenu_SetSelectedValue(panel.profiledd, currentProfile) + UIDropDownMenu_SetText(panel.profiledd, L["Current: "] .. currentProfile) panel.updown:SetChecked(settings.downclick) panel.fastooc:SetChecked(settings.fastooc) @@ -359,6 +363,7 @@ end function panel.okay() local settings = addon.settings + local currentProfile = addon.db:GetCurrentProfile() -- Update the saved variables settings.downclick = not not panel.updown:GetChecked() @@ -366,10 +371,11 @@ function panel.okay() settings.specswap = not not panel.specswap:GetChecked() settings.pri_profileKey = UIDropDownMenu_GetSelectedValue(panel.prispec) settings.sec_profileKey = UIDropDownMenu_GetSelectedValue(panel.secspec) - settings.profileKey = UIDropDownMenu_GetSelectedValue(panel.profiledd) + if newProfile ~= currentProfile then + addon.db:SetProfile(newProfile) + end addon:UpdateCombatWatch() - addon:ChangeProfile() end panel.cancel = panel.refresh diff --git a/libs/AceDB-3.0/AceDB-3.0.lua b/libs/AceDB-3.0/AceDB-3.0.lua new file mode 100644 index 0000000..7125643 --- /dev/null +++ b/libs/AceDB-3.0/AceDB-3.0.lua @@ -0,0 +1,728 @@ +--- **AceDB-3.0** manages the SavedVariables of your addon. +-- It offers profile management, smart defaults and namespaces for modules.\\ +-- Data can be saved in different data-types, depending on its intended usage. +-- The most common data-type is the `profile` type, which allows the user to choose +-- the active profile, and manage the profiles of all of his characters.\\ +-- The following data types are available: +-- * **char** Character-specific data. Every character has its own database. +-- * **realm** Realm-specific data. All of the players characters on the same realm share this database. +-- * **class** Class-specific data. All of the players characters of the same class share this database. +-- * **race** Race-specific data. All of the players characters of the same race share this database. +-- * **faction** Faction-specific data. All of the players characters of the same faction share this database. +-- * **factionrealm** Faction and realm specific data. All of the players characters on the same realm and of the same faction share this database. +-- * **global** Global Data. All characters on the same account share this database. +-- * **profile** Profile-specific data. All characters using the same profile share this database. The user can control which profile should be used. +-- +-- Creating a new Database using the `:New` function will return a new DBObject. A database will inherit all functions +-- of the DBObjectLib listed here. \\ +-- If you create a new namespaced child-database (`:RegisterNamespace`), you'll get a DBObject as well, but note +-- that the child-databases cannot individually change their profile, and are linked to their parents profile - and because of that, +-- the profile related APIs are not available. Only `:RegisterDefaults` and `:ResetProfile` are available on child-databases. +-- +-- For more details on how to use AceDB-3.0, see the [[AceDB-3.0 Tutorial]]. +-- +-- You may also be interested in [[libdualspec-1-0|LibDualSpec-1.0]] to do profile switching automatically when switching specs. +-- +-- @usage +-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("DBExample") +-- +-- -- declare defaults to be used in the DB +-- local defaults = { +-- profile = { +-- setting = true, +-- } +-- } +-- +-- function MyAddon:OnInitialize() +-- -- Assuming the .toc says ## SavedVariables: MyAddonDB +-- self.db = LibStub("AceDB-3.0"):New("MyAddonDB", defaults, true) +-- end +-- @class file +-- @name AceDB-3.0.lua +-- @release $Id: AceDB-3.0.lua 940 2010-06-19 08:01:47Z nevcairiel $ +local ACEDB_MAJOR, ACEDB_MINOR = "AceDB-3.0", 21 +local AceDB, oldminor = LibStub:NewLibrary(ACEDB_MAJOR, ACEDB_MINOR) + +if not AceDB then return end -- No upgrade needed + +-- Lua APIs +local type, pairs, next, error = type, pairs, next, error +local setmetatable, getmetatable, rawset, rawget = setmetatable, getmetatable, rawset, rawget + +-- WoW APIs +local _G = _G + +-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded +-- List them here for Mikk's FindGlobals script +-- GLOBALS: LibStub + +AceDB.db_registry = AceDB.db_registry or {} +AceDB.frame = AceDB.frame or CreateFrame("Frame") + +local CallbackHandler +local CallbackDummy = { Fire = function() end } + +local DBObjectLib = {} + +--[[------------------------------------------------------------------------- + AceDB Utility Functions +---------------------------------------------------------------------------]] + +-- Simple shallow copy for copying defaults +local function copyTable(src, dest) + if type(dest) ~= "table" then dest = {} end + if type(src) == "table" then + for k,v in pairs(src) do + if type(v) == "table" then + -- try to index the key first so that the metatable creates the defaults, if set, and use that table + v = copyTable(v, dest[k]) + end + dest[k] = v + end + end + return dest +end + +-- Called to add defaults to a section of the database +-- +-- When a ["*"] default section is indexed with a new key, a table is returned +-- and set in the host table. These tables must be cleaned up by removeDefaults +-- in order to ensure we don't write empty default tables. +local function copyDefaults(dest, src) + -- this happens if some value in the SV overwrites our default value with a non-table + --if type(dest) ~= "table" then return end + for k, v in pairs(src) do + if k == "*" or k == "**" then + if type(v) == "table" then + -- This is a metatable used for table defaults + local mt = { + -- This handles the lookup and creation of new subtables + __index = function(t,k) + if k == nil then return nil end + local tbl = {} + copyDefaults(tbl, v) + rawset(t, k, tbl) + return tbl + end, + } + setmetatable(dest, mt) + -- handle already existing tables in the SV + for dk, dv in pairs(dest) do + if not rawget(src, dk) and type(dv) == "table" then + copyDefaults(dv, v) + end + end + else + -- Values are not tables, so this is just a simple return + local mt = {__index = function(t,k) return k~=nil and v or nil end} + setmetatable(dest, mt) + end + elseif type(v) == "table" then + if not rawget(dest, k) then rawset(dest, k, {}) end + if type(dest[k]) == "table" then + copyDefaults(dest[k], v) + if src['**'] then + copyDefaults(dest[k], src['**']) + end + end + else + if rawget(dest, k) == nil then + rawset(dest, k, v) + end + end + end +end + +-- Called to remove all defaults in the default table from the database +local function removeDefaults(db, defaults, blocker) + -- remove all metatables from the db, so we don't accidentally create new sub-tables through them + setmetatable(db, nil) + -- loop through the defaults and remove their content + for k,v in pairs(defaults) do + if k == "*" or k == "**" then + if type(v) == "table" then + -- Loop through all the actual k,v pairs and remove + for key, value in pairs(db) do + if type(value) == "table" then + -- if the key was not explicitly specified in the defaults table, just strip everything from * and ** tables + if defaults[key] == nil and (not blocker or blocker[key] == nil) then + removeDefaults(value, v) + -- if the table is empty afterwards, remove it + if next(value) == nil then + db[key] = nil + end + -- if it was specified, only strip ** content, but block values which were set in the key table + elseif k == "**" then + removeDefaults(value, v, defaults[key]) + end + end + end + elseif k == "*" then + -- check for non-table default + for key, value in pairs(db) do + if defaults[key] == nil and v == value then + db[key] = nil + end + end + end + elseif type(v) == "table" and type(db[k]) == "table" then + -- if a blocker was set, dive into it, to allow multi-level defaults + removeDefaults(db[k], v, blocker and blocker[k]) + if next(db[k]) == nil then + db[k] = nil + end + else + -- check if the current value matches the default, and that its not blocked by another defaults table + if db[k] == defaults[k] and (not blocker or blocker[k] == nil) then + db[k] = nil + end + end + end +end + +-- This is called when a table section is first accessed, to set up the defaults +local function initSection(db, section, svstore, key, defaults) + local sv = rawget(db, "sv") + + local tableCreated + if not sv[svstore] then sv[svstore] = {} end + if not sv[svstore][key] then + sv[svstore][key] = {} + tableCreated = true + end + + local tbl = sv[svstore][key] + + if defaults then + copyDefaults(tbl, defaults) + end + rawset(db, section, tbl) + + return tableCreated, tbl +end + +-- Metatable to handle the dynamic creation of sections and copying of sections. +local dbmt = { + __index = function(t, section) + local keys = rawget(t, "keys") + local key = keys[section] + if key then + local defaultTbl = rawget(t, "defaults") + local defaults = defaultTbl and defaultTbl[section] + + if section == "profile" then + local new = initSection(t, section, "profiles", key, defaults) + if new then + -- Callback: OnNewProfile, database, newProfileKey + t.callbacks:Fire("OnNewProfile", t, key) + end + elseif section == "profiles" then + local sv = rawget(t, "sv") + if not sv.profiles then sv.profiles = {} end + rawset(t, "profiles", sv.profiles) + elseif section == "global" then + local sv = rawget(t, "sv") + if not sv.global then sv.global = {} end + if defaults then + copyDefaults(sv.global, defaults) + end + rawset(t, section, sv.global) + else + initSection(t, section, section, key, defaults) + end + end + + return rawget(t, section) + end +} + +local function validateDefaults(defaults, keyTbl, offset) + if not defaults then return end + offset = offset or 0 + for k in pairs(defaults) do + if not keyTbl[k] or k == "profiles" then + error(("Usage: AceDBObject:RegisterDefaults(defaults): '%s' is not a valid datatype."):format(k), 3 + offset) + end + end +end + +local preserve_keys = { + ["callbacks"] = true, + ["RegisterCallback"] = true, + ["UnregisterCallback"] = true, + ["UnregisterAllCallbacks"] = true, + ["children"] = true, +} + +local realmKey = GetRealmName() +local charKey = UnitName("player") .. " - " .. realmKey +local _, classKey = UnitClass("player") +local _, raceKey = UnitRace("player") +local factionKey = UnitFactionGroup("player") +local factionrealmKey = factionKey .. " - " .. realmKey +-- Actual database initialization function +local function initdb(sv, defaults, defaultProfile, olddb, parent) + -- Generate the database keys for each section + + -- map "true" to our "Default" profile + if defaultProfile == true then defaultProfile = "Default" end + + local profileKey + if not parent then + -- Make a container for profile keys + if not sv.profileKeys then sv.profileKeys = {} end + + -- Try to get the profile selected from the char db + profileKey = sv.profileKeys[charKey] or defaultProfile or charKey + + -- save the selected profile for later + sv.profileKeys[charKey] = profileKey + else + -- Use the profile of the parents DB + profileKey = parent.keys.profile or defaultProfile or charKey + + -- clear the profileKeys in the DB, namespaces don't need to store them + sv.profileKeys = nil + end + + -- This table contains keys that enable the dynamic creation + -- of each section of the table. The 'global' and 'profiles' + -- have a key of true, since they are handled in a special case + local keyTbl= { + ["char"] = charKey, + ["realm"] = realmKey, + ["class"] = classKey, + ["race"] = raceKey, + ["faction"] = factionKey, + ["factionrealm"] = factionrealmKey, + ["profile"] = profileKey, + ["global"] = true, + ["profiles"] = true, + } + + validateDefaults(defaults, keyTbl, 1) + + -- This allows us to use this function to reset an entire database + -- Clear out the old database + if olddb then + for k,v in pairs(olddb) do if not preserve_keys[k] then olddb[k] = nil end end + end + + -- Give this database the metatable so it initializes dynamically + local db = setmetatable(olddb or {}, dbmt) + + if not rawget(db, "callbacks") then + -- try to load CallbackHandler-1.0 if it loaded after our library + if not CallbackHandler then CallbackHandler = LibStub:GetLibrary("CallbackHandler-1.0", true) end + db.callbacks = CallbackHandler and CallbackHandler:New(db) or CallbackDummy + end + + -- Copy methods locally into the database object, to avoid hitting + -- the metatable when calling methods + + if not parent then + for name, func in pairs(DBObjectLib) do + db[name] = func + end + else + -- hack this one in + db.RegisterDefaults = DBObjectLib.RegisterDefaults + db.ResetProfile = DBObjectLib.ResetProfile + end + + -- Set some properties in the database object + db.profiles = sv.profiles + db.keys = keyTbl + db.sv = sv + --db.sv_name = name + db.defaults = defaults + db.parent = parent + + -- store the DB in the registry + AceDB.db_registry[db] = true + + return db +end + +-- handle PLAYER_LOGOUT +-- strip all defaults from all databases +-- and cleans up empty sections +local function logoutHandler(frame, event) + if event == "PLAYER_LOGOUT" then + for db in pairs(AceDB.db_registry) do + db.callbacks:Fire("OnDatabaseShutdown", db) + db:RegisterDefaults(nil) + + -- cleanup sections that are empty without defaults + local sv = rawget(db, "sv") + for section in pairs(db.keys) do + if rawget(sv, section) then + -- global is special, all other sections have sub-entrys + -- also don't delete empty profiles on main dbs, only on namespaces + if section ~= "global" and (section ~= "profiles" or rawget(db, "parent")) then + for key in pairs(sv[section]) do + if not next(sv[section][key]) then + sv[section][key] = nil + end + end + end + if not next(sv[section]) then + sv[section] = nil + end + end + end + end + end +end + +AceDB.frame:RegisterEvent("PLAYER_LOGOUT") +AceDB.frame:SetScript("OnEvent", logoutHandler) + + +--[[------------------------------------------------------------------------- + AceDB Object Method Definitions +---------------------------------------------------------------------------]] + +--- Sets the defaults table for the given database object by clearing any +-- that are currently set, and then setting the new defaults. +-- @param defaults A table of defaults for this database +function DBObjectLib:RegisterDefaults(defaults) + if defaults and type(defaults) ~= "table" then + error("Usage: AceDBObject:RegisterDefaults(defaults): 'defaults' - table or nil expected.", 2) + end + + validateDefaults(defaults, self.keys) + + -- Remove any currently set defaults + if self.defaults then + for section,key in pairs(self.keys) do + if self.defaults[section] and rawget(self, section) then + removeDefaults(self[section], self.defaults[section]) + end + end + end + + -- Set the DBObject.defaults table + self.defaults = defaults + + -- Copy in any defaults, only touching those sections already created + if defaults then + for section,key in pairs(self.keys) do + if defaults[section] and rawget(self, section) then + copyDefaults(self[section], defaults[section]) + end + end + end +end + +--- Changes the profile of the database and all of it's namespaces to the +-- supplied named profile +-- @param name The name of the profile to set as the current profile +function DBObjectLib:SetProfile(name) + if type(name) ~= "string" then + error("Usage: AceDBObject:SetProfile(name): 'name' - string expected.", 2) + end + + -- changing to the same profile, dont do anything + if name == self.keys.profile then return end + + local oldProfile = self.profile + local defaults = self.defaults and self.defaults.profile + + -- Callback: OnProfileShutdown, database + self.callbacks:Fire("OnProfileShutdown", self) + + if oldProfile and defaults then + -- Remove the defaults from the old profile + removeDefaults(oldProfile, defaults) + end + + self.profile = nil + self.keys["profile"] = name + + -- if the storage exists, save the new profile + -- this won't exist on namespaces. + if self.sv.profileKeys then + self.sv.profileKeys[charKey] = name + end + + -- populate to child namespaces + if self.children then + for _, db in pairs(self.children) do + DBObjectLib.SetProfile(db, name) + end + end + + -- Callback: OnProfileChanged, database, newProfileKey + self.callbacks:Fire("OnProfileChanged", self, name) +end + +--- Returns a table with the names of the existing profiles in the database. +-- You can optionally supply a table to re-use for this purpose. +-- @param tbl A table to store the profile names in (optional) +function DBObjectLib:GetProfiles(tbl) + if tbl and type(tbl) ~= "table" then + error("Usage: AceDBObject:GetProfiles(tbl): 'tbl' - table or nil expected.", 2) + end + + -- Clear the container table + if tbl then + for k,v in pairs(tbl) do tbl[k] = nil end + else + tbl = {} + end + + local curProfile = self.keys.profile + + local i = 0 + for profileKey in pairs(self.profiles) do + i = i + 1 + tbl[i] = profileKey + if curProfile and profileKey == curProfile then curProfile = nil end + end + + -- Add the current profile, if it hasn't been created yet + if curProfile then + i = i + 1 + tbl[i] = curProfile + end + + return tbl, i +end + +--- Returns the current profile name used by the database +function DBObjectLib:GetCurrentProfile() + return self.keys.profile +end + +--- Deletes a named profile. This profile must not be the active profile. +-- @param name The name of the profile to be deleted +-- @param silent If true, do not raise an error when the profile does not exist +function DBObjectLib:DeleteProfile(name, silent) + if type(name) ~= "string" then + error("Usage: AceDBObject:DeleteProfile(name): 'name' - string expected.", 2) + end + + if self.keys.profile == name then + error("Cannot delete the active profile in an AceDBObject.", 2) + end + + if not rawget(self.profiles, name) and not silent then + error("Cannot delete profile '" .. name .. "'. It does not exist.", 2) + end + + self.profiles[name] = nil + + -- populate to child namespaces + if self.children then + for _, db in pairs(self.children) do + DBObjectLib.DeleteProfile(db, name, true) + end + end + + -- Callback: OnProfileDeleted, database, profileKey + self.callbacks:Fire("OnProfileDeleted", self, name) +end + +--- Copies a named profile into the current profile, overwriting any conflicting +-- settings. +-- @param name The name of the profile to be copied into the current profile +-- @param silent If true, do not raise an error when the profile does not exist +function DBObjectLib:CopyProfile(name, silent) + if type(name) ~= "string" then + error("Usage: AceDBObject:CopyProfile(name): 'name' - string expected.", 2) + end + + if name == self.keys.profile then + error("Cannot have the same source and destination profiles.", 2) + end + + if not rawget(self.profiles, name) and not silent then + error("Cannot copy profile '" .. name .. "'. It does not exist.", 2) + end + + -- Reset the profile before copying + DBObjectLib.ResetProfile(self, nil, true) + + local profile = self.profile + local source = self.profiles[name] + + copyTable(source, profile) + + -- populate to child namespaces + if self.children then + for _, db in pairs(self.children) do + DBObjectLib.CopyProfile(db, name, true) + end + end + + -- Callback: OnProfileCopied, database, sourceProfileKey + self.callbacks:Fire("OnProfileCopied", self, name) +end + +--- Resets the current profile to the default values (if specified). +-- @param noChildren if set to true, the reset will not be populated to the child namespaces of this DB object +-- @param noCallbacks if set to true, won't fire the OnProfileReset callback +function DBObjectLib:ResetProfile(noChildren, noCallbacks) + local profile = self.profile + + for k,v in pairs(profile) do + profile[k] = nil + end + + local defaults = self.defaults and self.defaults.profile + if defaults then + copyDefaults(profile, defaults) + end + + -- populate to child namespaces + if self.children and not noChildren then + for _, db in pairs(self.children) do + DBObjectLib.ResetProfile(db, nil, noCallbacks) + end + end + + -- Callback: OnProfileReset, database + if not noCallbacks then + self.callbacks:Fire("OnProfileReset", self) + end +end + +--- Resets the entire database, using the string defaultProfile as the new default +-- profile. +-- @param defaultProfile The profile name to use as the default +function DBObjectLib:ResetDB(defaultProfile) + if defaultProfile and type(defaultProfile) ~= "string" then + error("Usage: AceDBObject:ResetDB(defaultProfile): 'defaultProfile' - string or nil expected.", 2) + end + + local sv = self.sv + for k,v in pairs(sv) do + sv[k] = nil + end + + local parent = self.parent + + initdb(sv, self.defaults, defaultProfile, self) + + -- fix the child namespaces + if self.children then + if not sv.namespaces then sv.namespaces = {} end + for name, db in pairs(self.children) do + if not sv.namespaces[name] then sv.namespaces[name] = {} end + initdb(sv.namespaces[name], db.defaults, self.keys.profile, db, self) + end + end + + -- Callback: OnDatabaseReset, database + self.callbacks:Fire("OnDatabaseReset", self) + -- Callback: OnProfileChanged, database, profileKey + self.callbacks:Fire("OnProfileChanged", self, self.keys["profile"]) + + return self +end + +--- Creates a new database namespace, directly tied to the database. This +-- is a full scale database in it's own rights other than the fact that +-- it cannot control its profile individually +-- @param name The name of the new namespace +-- @param defaults A table of values to use as defaults +function DBObjectLib:RegisterNamespace(name, defaults) + if type(name) ~= "string" then + error("Usage: AceDBObject:RegisterNamespace(name, defaults): 'name' - string expected.", 2) + end + if defaults and type(defaults) ~= "table" then + error("Usage: AceDBObject:RegisterNamespace(name, defaults): 'defaults' - table or nil expected.", 2) + end + if self.children and self.children[name] then + error ("Usage: AceDBObject:RegisterNamespace(name, defaults): 'name' - a namespace with that name already exists.", 2) + end + + local sv = self.sv + if not sv.namespaces then sv.namespaces = {} end + if not sv.namespaces[name] then + sv.namespaces[name] = {} + end + + local newDB = initdb(sv.namespaces[name], defaults, self.keys.profile, nil, self) + + if not self.children then self.children = {} end + self.children[name] = newDB + return newDB +end + +--- Returns an already existing namespace from the database object. +-- @param name The name of the new namespace +-- @param silent if true, the addon is optional, silently return nil if its not found +-- @usage +-- local namespace = self.db:GetNamespace('namespace') +-- @return the namespace object if found +function DBObjectLib:GetNamespace(name, silent) + if type(name) ~= "string" then + error("Usage: AceDBObject:GetNamespace(name): 'name' - string expected.", 2) + end + if not silent and not (self.children and self.children[name]) then + error ("Usage: AceDBObject:GetNamespace(name): 'name' - namespace does not exist.", 2) + end + if not self.children then self.children = {} end + return self.children[name] +end + +--[[------------------------------------------------------------------------- + AceDB Exposed Methods +---------------------------------------------------------------------------]] + +--- Creates a new database object that can be used to handle database settings and profiles. +-- By default, an empty DB is created, using a character specific profile. +-- +-- You can override the default profile used by passing any profile name as the third argument, +-- or by passing //true// as the third argument to use a globally shared profile called "Default". +-- +-- Note that there is no token replacement in the default profile name, passing a defaultProfile as "char" +-- will use a profile named "char", and not a character-specific profile. +-- @param tbl The name of variable, or table to use for the database +-- @param defaults A table of database defaults +-- @param defaultProfile The name of the default profile. If not set, a character specific profile will be used as the default. +-- You can also pass //true// to use a shared global profile called "Default". +-- @usage +-- -- Create an empty DB using a character-specific default profile. +-- self.db = LibStub("AceDB-3.0"):New("MyAddonDB") +-- @usage +-- -- Create a DB using defaults and using a shared default profile +-- self.db = LibStub("AceDB-3.0"):New("MyAddonDB", defaults, true) +function AceDB:New(tbl, defaults, defaultProfile) + if type(tbl) == "string" then + local name = tbl + tbl = _G[name] + if not tbl then + tbl = {} + _G[name] = tbl + end + end + + if type(tbl) ~= "table" then + error("Usage: AceDB:New(tbl, defaults, defaultProfile): 'tbl' - table expected.", 2) + end + + if defaults and type(defaults) ~= "table" then + error("Usage: AceDB:New(tbl, defaults, defaultProfile): 'defaults' - table expected.", 2) + end + + if defaultProfile and type(defaultProfile) ~= "string" and defaultProfile ~= true then + error("Usage: AceDB:New(tbl, defaults, defaultProfile): 'defaultProfile' - string or true expected.", 2) + end + + return initdb(tbl, defaults, defaultProfile) +end + +-- upgrade existing databases +for db in pairs(AceDB.db_registry) do + if not db.parent then + for name,func in pairs(DBObjectLib) do + db[name] = func + end + else + db.RegisterDefaults = DBObjectLib.RegisterDefaults + db.ResetProfile = DBObjectLib.ResetProfile + end +end diff --git a/libs/AceDB-3.0/AceDB-3.0.xml b/libs/AceDB-3.0/AceDB-3.0.xml new file mode 100644 index 0000000..e825f7b --- /dev/null +++ b/libs/AceDB-3.0/AceDB-3.0.xml @@ -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="AceDB-3.0.lua"/> +</Ui> \ No newline at end of file diff --git a/libs/CallbackHandler-1.0/CallbackHandler-1.0.lua b/libs/CallbackHandler-1.0/CallbackHandler-1.0.lua new file mode 100644 index 0000000..bc311d9 --- /dev/null +++ b/libs/CallbackHandler-1.0/CallbackHandler-1.0.lua @@ -0,0 +1,240 @@ +--[[ $Id: CallbackHandler-1.0.lua 965 2010-08-09 00:47:52Z 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, 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" 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/CallbackHandler-1.0/CallbackHandler-1.0.xml b/libs/CallbackHandler-1.0/CallbackHandler-1.0.xml new file mode 100644 index 0000000..1aad3a2 --- /dev/null +++ b/libs/CallbackHandler-1.0/CallbackHandler-1.0.xml @@ -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> \ No newline at end of file diff --git a/libs/LibStub/LibStub.lua b/libs/LibStub/LibStub.lua new file mode 100644 index 0000000..cfc97de --- /dev/null +++ b/libs/LibStub/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