view LibModuleDBShare-1.0/LibModuleDBShare-1.0.lua @ 48:a686af703642 tip

Added tag v1.3.1 release for changeset c48d2d940e82
author Andrew Knoll <andrewtknoll@gmail.com>
date Fri, 12 Apr 2013 22:59:20 -0400
parents c48d2d940e82
children
line wrap: on
line source
--- **LibModuleDBShare-1.0** provides a shared profile manager for addons without a central core.
-- A basic options panel for the group is added to the Blizzard options panel, as well as a
-- standard profile manager as a subpanel. Changes through the profiles panel are propagated
-- to member databases. The root panel can be used as a parent for your module config panels,
-- to keep all your addon's config in one place. The root panel's name is the same as the group's
-- name.\\
-- \\
-- A group can be created using the ':NewGroup' library method. The returned object inherits all
-- methods of the DBGroup object described below.\\
-- \\
-- **LibDualSpec Support**\\
-- LibModuleDBShare can use LibDualSpec to manage automatic profile switching with talent spec
-- changes. This integration is handled by the library; there is no need to use LibDualSpec
-- on member databases directly.\\
-- \\
-- **Slash Command Support**\\
-- LibModuleDBShare can associate a slash command with a DBGroup. The default handler function
-- for the slash command opens the root options panel.\\
-- Additional handler functions can be registered to respond to specific arguments given to the
-- slash command. If you provide a custom handler function for the slash command, that function
-- is responsible for detecting secondary commands.
--
-- @usage
-- local database;
-- -- this function is called after the ADDON_LOADED event fires
-- function initializeDB()
--     database = LibStub("AceDB-3.0"):New("MyAddonDB", defaults, true);
--     local group = LibStub("LibModuleDBShare-1.0"):GetGroup("Group Name");
--     if not group then
--         group = LibStub("LibModuleDBShare-1.0"):NewGroup("Group Name", "A description for this group.", database);
--     else
--         group:AddDB(database);
--     end
--     -- if you want to add a slash command
--     if not group:HasSlashCommand() then
--         group:EnableSlashCommand("COMMAND_NAME", "/groupname");
--     end
-- end
-- @class file
-- @name LibModuleDBShare-1.0
local MAJOR, MINOR = "LibModuleDBShare-1.0", 6
local LibModuleDBShare, oldminor = LibStub:NewLibrary(MAJOR, MINOR)

if not LibModuleDBShare then return end -- No upgrade needed

-- Lua functions
local error, type, pairs, time = error, type, pairs, time;

-- Required Libraries
local AceDB = LibStub("AceDB-3.0");
local AceDBOptions = LibStub("AceDBOptions-3.0");
local AceConfigRegistry = LibStub("AceConfigRegistry-3.0");
local AceConfigDialog = LibStub("AceConfigDialog-3.0");

-- Optional Libraries
local LibDualSpec = LibStub("LibDualSpec-1.0", true);

LibModuleDBShare.groups = LibModuleDBShare.groups or {};

local DBGroup = {};

--- Creates a new DB group.
-- @paramsig groupName, groupDescription, initialDB[, usesDualSpec]
-- @param groupName The name of the new DB group, as shown in the options panel. (string)
-- @param groupDescription A description of the group to be shown in the root options panel. (string)
-- @param initialDB The first DB to add to the group. (table)
-- @param usesDualSpec True if this group should use LibDualSpec, false otherwise. (boolean or nil)
-- @return the new DB group object (table)
function LibModuleDBShare:NewGroup(groupName, groupDescription, initialDB, usesDualSpec)
	-- check to see if LibDualSpec has been loaded
	if not LibDualSpec then
		LibDualSpec = LibStub("LibDualSpec-1.0", true);
	end
	-- verify parameters
	if type(groupName) ~= "string" then
		error("Usage: LibModuleDBShare:NewGroup(groupName, groupDescription, initialDB, usesDualSpec): 'groupName' must be a string.", 2);
	elseif type(groupDescription) ~= "string" then
		error("Usage: LibModuleDBShare:NewGroup(groupName, groupDescription, initialDB, usesDualSpec): 'groupDescription' must be a string.", 2);
	elseif type(LibModuleDBShare.groups[groupName]) ~= "nil" then
		error("Usage: LibModuleDBShare:NewGroup(groupName, groupDescription, initialDB, usesDualSpec): group '"..groupName.."' already exists.", 2);
	elseif type(initialDB) ~= "table" or not AceDB.db_registry[initialDB] then
		error("Usage: LibModuleDBShare:NewGroup(groupName, groupDescription, initialDB, usesDualSpec): 'initialDB' must be an AceDB-3.0 database.", 2);
	elseif initialDB.parent then
		error("Usage: LibModuleDBShare:NewGroup(groupName, groupDescription, initialDB, usesDualSpec): 'initialDB' must not be a namespace.", 2)
	elseif type(usesDualSpec) ~= "boolean" and type(usesDualSpec) ~= "nil" then
		error("Usage: LibModuleDBShare:NewGroup(groupName, groupDescription, initialDB, usesDualSpec): 'usesDualSpec' must be a boolean or nil.", 2);
	elseif usesDualSpec and not LibDualSpec then
		error("Usage: LibModuleDBShare:NewGroup(groupName, groupDescription, initialDB, usesDualSpec): 'usesDualSpec' cannot be true without LibDualSpec-1.0 installed.", 2);
	end
	-- create group
	local group = {}
	group.name = groupName;
	group.members = {};
	-- create root option panel for group
	group.rootOptionsTable = {
		type = "group",
		name = groupName,
		args = {
			text = {
				type = "description",
				name = groupDescription,
			},
		},
	};
	AceConfigRegistry:RegisterOptionsTable(groupName, group.rootOptionsTable);
	AceConfigDialog:AddToBlizOptions(groupName);
	-- create sync DB and profile options page
	group.syncDBTable = {};
	group.syncDB = AceDB:New(group.syncDBTable, nil, initialDB:GetCurrentProfile());
	group.profileOptionsTable = AceDBOptions:GetOptionsTable(group.syncDB, false);
	if usesDualSpec then
		group.usesDualSpec = true;
		LibDualSpec:EnhanceDatabase(group.syncDB, groupName);
		LibDualSpec:EnhanceOptions(group.profileOptionsTable, group.syncDB);
	else
		group.usesDualSpec = false;
	end
	AceConfigRegistry:RegisterOptionsTable(groupName.."Profiles", group.profileOptionsTable);
	AceConfigDialog:AddToBlizOptions(groupName.."Profiles", group.profileOptionsTable.name, groupName);
	-- add all profiles from initialDB to syncDB
	for i, profile in pairs(initialDB:GetProfiles()) do
		group.syncDB:SetProfile(profile);
	end
	-- load profile info from initialDB
	group.syncDB:SetProfile(initialDB:GetCurrentProfile());
	group.members[initialDB] = initialDB:GetNamespace(MAJOR, true) or initialDB:RegisterNamespace(MAJOR);
	local storedData = group.members[initialDB].char;
	if type(storedData.logoutTimestamp) == "number" then
		group.profileTimestamp = storedData.logoutTimestamp;
	else
		group.profileTimestamp = 0;
	end
	if usesDualSpec then
		local LDSnamespace = group.syncDB:GetNamespace("LibDualSpec-1.0");
		LDSnamespace.char.enabled = storedData.dualSpecEnabled;
		LDSnamespace.char.profile = storedData.altProfile;
		LDSnamespace.char.specGroup = storedData.activeSpecGroup;
		group.syncDB:CheckDualSpecState();
	else
		group.syncDB.char.enabled = storedData.dualSpecEnabled;
		group.syncDB.char.profile = storedData.altProfile;
		group.syncDB.char.specGroup = storedData.activeSpecGroup;
	end
	-- add methods and callbacks
	for k, v in pairs(DBGroup) do
		group[k] = v;
	end
	group.syncDB.RegisterCallback(group, "OnProfileChanged", "OnProfileChanged");
	group.syncDB.RegisterCallback(group, "OnProfileDeleted", "OnProfileDeleted");
	group.syncDB.RegisterCallback(group, "OnProfileCopied", "OnProfileCopied");
	group.syncDB.RegisterCallback(group, "OnProfileReset", "OnProfileReset");
	group.syncDB.RegisterCallback(group, "OnDatabaseShutdown", "OnSyncShutdown");
	group.members[initialDB].RegisterCallback(group, "OnDatabaseShutdown", "OnMemberShutdown"); -- register the namespace, not the base db
	group.squelchCallbacks = false;
	LibModuleDBShare.groups[groupName] = group;
	return group;
end

--- Retrieves an existing DB group.
-- @param groupName The name of the DB group to retrieve. (string)
-- @return the DB group object, or ##nil## if not found (table)
function LibModuleDBShare:GetGroup(groupName)
	if type(groupName) ~= "string" then
		error("Usage: LibModuleDBShare:GetGroup(groupName): 'groupName' must be a string.", 2);
	end
	return LibModuleDBShare.groups[groupName];
end

--- Adds a database to the group.
-- @param newDB The database to add. (table)
function DBGroup:AddDB(newDB)
	-- verify parameters
	if type(newDB) ~= "table" or not AceDB.db_registry[newDB] then
		error("Usage: DBGroup:AddDB(newDB): 'newDB' must be an AceDB-3.0 database.", 2);
	elseif newDB.parent then
		error("Usage: DBGroup:AddDB(newDB): 'newDB' must not be a namespace.", 2)
	elseif type(self.members[newDB]) ~= "nil" then
		error("Usage: DBGroup:AddDB(newDB): 'newDB' is already a member of DBGroup.", 2);
	end
	for groupName, group in pairs(LibModuleDBShare.groups) do
		if group.members[newDB] ~= nil then
			error("Usage: DBGroup:AddDB(newDB): 'newDB' is already a member of group '"..groupName.."'.", 2);
		end
	end
	-- record current profile
	local syncProfile = self.syncDB:GetCurrentProfile();
	-- add new profiles to syncDB
	self.squelchCallbacks = true;
	for i, profile in pairs(newDB:GetProfiles()) do
		self.syncDB:SetProfile(profile);
	end
	-- set current profile based on timestamps
	local namespace = newDB:GetNamespace(MAJOR, true) or newDB:RegisterNamespace(MAJOR);
	local storedData = namespace.char;
	if type(storedData.logoutTimestamp) == "number" and storedData.logoutTimestamp > self.profileTimestamp then
		self.squelchCallbacks = false;
		self.syncDB:SetProfile(newDB:GetCurrentProfile());
		self.profileTimestamp = storedData.logoutTimestamp;
		if self.usesDualSpec and storedData.altProfile then
			local LDSnamespace = group.syncDB:GetNamespace("LibDualSpec-1.0");
			LDSnamespace.char.enabled = storedData.dualSpecEnabled;
			LDSnamespace.char.profile = storedData.altProfile;
			LDSnamespace.char.specGroup = storedData.activeSpecGroup;
			group.syncDB:CheckDualSpecState();
		elseif storedData.altProfile then
			self.syncDB.char.enabled = storedData.dualSpecEnabled;
			self.syncDB.char.profile = storedData.altProfile;
			self.syncDB.char.specGroup = storedData.activeSpecGroup;
		end
	else
		self.syncDB:SetProfile(syncProfile);
		newDB:SetProfile(syncProfile);
		self.squelchCallbacks = false;
	end
	-- add to members list
	self.members[newDB] = namespace;
	namespace.RegisterCallback(self, "OnDatabaseShutdown", "OnMemberShutdown"); -- register the namespace, not the base db
end

-- LibDualSpec support

--- Checks to see if this group uses LibDualSpec.
-- @return ##true## if this group uses LibDualSpec, ##false## otherwise (boolean)
function DBGroup:IsUsingDualSpec()
	return self.usesDualSpec;
end

--- Enables dual spec support if not already enabled.
function DBGroup:EnableDualSpec()
	if not LibDualSpec then
		LibDualSpec = LibStub("LibDualSpec-1.0"); -- this will error if LDS isn't found
	end
	if not self.usesDualSpec then
		LibDualSpec:EnhanceDatabase(self.syncDB, self.name);
		LibDualSpec:EnhanceOptions(self.profileOptionsTable, self.syncDB);
		AceConfigRegistry:NotifyChange(self.name.."Profiles");
		self.usesDualSpec = true;
		local namespace = self.syncDB:GetNamespace("LibDualSpec-1.0");
		namespace.char.enabled = self.syncDB.char.enabled;
		namespace.char.profile = self.syncDB.char.profile;
		namespace.char.specGroup = self.syncDB.char.specGroup;
		self.syncDB:CheckDualSpecState();
	end
end

-- slash command support

--- Adds a slash command to the group.
-- @paramsig slug, commandList[, handler]
-- @param slug The base identifier to use for the slash command. (string)
-- @param commandList The command itself, or a list of commands to use. (string or table)
-- @param handler A handler function for the command. If nil, defaults to a function that
-- calls the appropriate secondary command, or opens the root options panel. (function)
function DBGroup:EnableSlashCommand(slug, commandList, handler)
	if self.slug then
		error("Usage: DBGroup:EnableSlashCommand(slug, commandList[, handler]): group already has a slash command.", 2);
	elseif type(slug) ~= "string" then
		error("Usage: DBGroup:EnableSlashCommand(slug, commandList[, handler]): 'slug' must be a string.", 2);
	elseif type(commandList) ~= "string" and type(commandList) ~= "table" then
		error("Usage: DBGroup:EnableSlashCommand(slug, commandList[, handler]): 'commandList' must be a string or table.", 2);
	elseif handler and type(handler) ~= "function" then
		error("Usage: DBGroup:EnableSlashCommand(slug, commandList[, handler]): 'handler' must be nil or a function.", 2);
	elseif type(commandList) == "table" then
		for i = 1, #commandList do
			if type(commandList[i]) ~= "string" then
				error("Usage: DBGroup:EnableSlashCommand(slug, commandList[, handler]): 'commandList' must contain only strings.", 2);
			end
		end
	end
	
	self.slug = slug;
	self.subCmdList = {};
	if type(commandList) == "string" then
		_G["SLASH_"..slug.."1"] = commandList;
	else
		for i = 1, #commandList do
			_G["SLASH_"..slug..i] = commandList[i];
		end
	end
	
	if handler then
		SlashCmdList[slug] = handler;
	else
		SlashCmdList[slug] = function(msg, editBox)
			for cmd, func in pairs(self.subCmdList) do
				if msg == cmd then
					func("", editBox);
					return;
				elseif msg:len() > cmd:len() then
					if msg:sub(1, cmd:len() + 1) == (cmd.." ") then
						func(msg:sub(cmd:len() + 2), editBox);
						return;
					end
				end
			end
		
			for k, button in pairs(InterfaceOptionsFrameAddOns.buttons) do
				if button.element and button.element.name == self.name and button.element.collapsed then
					OptionsListButtonToggle_OnClick(button.toggle);
					break;
				end
			end
			InterfaceOptionsFrame_OpenToCategory(self.name);
		end;
	end
end

--- Checks to see if this group has a slash command.
-- @return ##true## if this group has a slash command, ##false## otherwise (boolean)
function DBGroup:HasSlashCommand()
	if self.slug then
		return true;
	else
		return false;
	end
end

--- Adds an alias for the slash command.
-- @param alias The alternate name for the slash command. (string)
function DBGroup:AddSlashCommandAlias(alias)
	if type(alias) ~= "string" then
		error("Usage: DBGroup:AddSlashCommandAlias(alias): 'alias' must be a string.", 2);
	elseif not self.slug then
		error("Usage: DBGroup:AddSlashCommandAlias(alias): slash commands for this group have not be enabled.", 2);
	end
	
	local i = 1;
	while _G["SLASH_"..self.slug..i] do
		i = i + 1;
	end
	
	_G["SLASH_"..self.slug..i] = alias;
end

--- Adds a secondary command handler to the slash command for this group.
-- This handler will be called if the argument to the slash command matches the name provided.
-- @paramsig name, handler[, silent]
-- @param name The name of the secondary command. (string)
-- @param handler The function to handle the command. (function)
-- @param silent ##True## if you want to replace the currently registered command, ##false##
-- otherwise. (boolean)
function DBGroup:AddSecondaryCommand(name, handler, silent)
	if type(name) ~= "string" then
		error("Usage: DBGroup:AddSecondaryCommand(name, handler[, overwrite]): 'name' must be a string.", 2);
	elseif type(handler) ~= "function" then
		error("Usage: DBGroup:AddSecondaryCommand(name, handler[, overwrite]): 'handler' must be a function.", 2);
	elseif not self.slug then
		error("Usage: DBGroup:AddSecondaryCommand(name, handler[, overwrite]): slash commands for this group have not be enabled.", 2);
	elseif type(overwrite) ~= "boolean" and type(overwrite) ~= "nil" then
		error("Usage: DBGroup:AddSecondaryCommand(name, handler[, overwrite]): 'overwrite' must be a boolean or nil", 2);
	end
	if not silent then
		for k, v in pairs(self.subCmdList) do
			if k == name then
				error("Usage: DBGroup:AddSecondaryCommand(name, handler[, overwrite]): command '"..name.."' already exists.", 2);
			end
		end
	end
		
	self.subCmdList[name] = handler;
end

--- Returns the list of secondary commands registered with this group.
-- @return A table containing name-function pairs for secondary commands. (table)
function DBGroup:GetSecondaryCommands()
	if not self.slug then
		error("Usage: DBGroup:GetSecondaryCommands(): Slash commands for this group have not been enabled", 2);
	end
	return self.subCmdList;
end

-- callback handlers (new profiles are handled by OnProfileChanged)

function DBGroup:OnProfileChanged(callback, syncDB, profile)
	if not self.squelchCallbacks then
		for db, _ in pairs(self.members) do
			db:SetProfile(profile);
		end
	end
end

function DBGroup:OnProfileDeleted(callback, syncDB, profile)
	for db, _ in pairs(self.members) do
		db:DeleteProfile(profile, true);
	end
end

function DBGroup:OnProfileCopied(callback, syncDB, profile)
	for db, _ in pairs(self.members) do
		db:CopyProfile(profile, true);
	end
end

function DBGroup:OnProfileReset(callback, syncDB)
	for db, _ in pairs(self.members) do
		db:ResetProfile(false, false);
	end
end

-- shutdown handling

local altProfile = nil;
local dualSpecEnabled = nil;
local activeSpecGroup = nil;

function DBGroup:OnSyncShutdown(callback, syncDB)
	if self.usesDualSpec and not altProfile then
		altProfile = syncDB:GetDualSpecProfile();
		dualSpecEnabled = syncDB:IsDualSpecEnabled();
		activeSpecGroup = GetActiveSpecGroup();
	end
end

local timestamp = nil;

function DBGroup:OnMemberShutdown(callback, db)
	if not timestamp then	-- ensure uniform timestamps to minimize
		timestamp = time();	-- calls to SetProfile in NewGroup
	end
	db.char.logoutTimestamp = timestamp; -- namespace is registered for callback, not base db
	if self.usesDualSpec then
		if not altProfile then
			altProfile = self.syncDB:GetDualSpecProfile();
			dualSpecEnabled = self.syncDB:IsDualSpecEnabled();
			activeSpecGroup = GetActiveSpecGroup();
		end
		db.char.altProfile = altProfile;
		db.char.dualSpecEnabled = dualSpecEnabled;
		db.char.activeSpecGroup = activeSpecGroup;
	end
end

-- update existing groups
for groupName, group in pairs(LibModuleDBShare.groups) do
	for funcName, func in pairs(DBGroup) do
		group[funcName] = func;
	end
end