view ReAction.lua @ 64:2000f4f4c6af

Redesigned state interface. The only thing missing now is the actual state properties.
author Flick <flickerstreak@gmail.com>
date Wed, 28 May 2008 00:20:04 +0000
parents 768be7eb22a0
children 06cd74bdc7da
line wrap: on
line source
--[[
  ReAction.lua

  The ReAction core manages 4 collections:
    - modules (via AceAddon)
    - bars
    - options
    - bar-type constructors
    
  and publishes events when those collections change. It also implements a single property, 'config mode',
  and has a couple convenience methods which drill down to particular modules.
  
  Most of the "real work" of the addon happens in Bar.lua and the various modules.

  Events (with handler arguments):
  --------------------------------
  "OnCreateBar" (bar, name)             : after a bar object is created
  "OnDestroyBar" (bar, name)            : before a bar object is destroyed
  "OnEraseBar" (bar, name)              : before a bar config is removed from the profile db
  "OnRenameBar" (bar, oldname, newname) : after a bar is renamed
  "OnRefreshBar" (bar, name)            : after a bar's state has been updated
  "OnOptionsRefreshed" ()               : after the global options tree is refreshed
  "OnConfigModeChanged" (mode)          : after the config mode is changed
  "OnBarOptionGeneratorRegistered" (module, function) : after an options generator function is registered

  ReAction is also an AceAddon-3.0 and contains an AceDB-3.0, which in turn publish more events.
]]--
local version = GetAddOnMetadata("ReAction","Version")

------ CORE ------
local ReAction = LibStub("AceAddon-3.0"):NewAddon( "ReAction",
  "AceConsole-3.0",
  "AceEvent-3.0"
)
ReAction.revision = tonumber(("$Revision$"):match("%d+"))

------ GLOBALS ------
_G["ReAction"] = ReAction

------ DEBUGGING ------
ReAction.debug = true
local dbprint
if ReAction.debug then
  dbprint = function(msg)
    DEFAULT_CHAT_FRAME:AddMessage(msg)
  end
else
  dbprint = function() end
end
ReAction.dbprint = dbprint

------ LIBRARIES ------
local callbacks = LibStub("CallbackHandler-1.0"):New(ReAction)
local L = LibStub("AceLocale-3.0"):GetLocale("ReAction")
ReAction.L = L

------ PRIVATE ------
local private = { }
local bars = {}
local defaultBarConfig = {}
local barOptionGenerators = { }
local options = {
  type = "group",
  name = "ReAction",
  childGroups = "tab",
  args = {
    _desc = {
      type = "description",
      name = L["Customizable replacement for Blizzard's Action Bars"],
      order = 1,
    },
    global = {
      type = "group",
      name = L["Global Settings"],
      desc = L["Global configuration settings"],
      args = { 
        unlock = {
          type     = "toggle",
          name     = L["Unlock Bars"],
          desc     = L["Unlock bars for dragging and resizing with the mouse"],
          handler  = ReAction,
          get      = "GetConfigMode",
          set      = function(info, value) ReAction:SetConfigMode(value) end,
          disabled = InCombatLockdown,
          order    = 1
        },
      },
      plugins = { },
      order = 2,
    },
    module = {
      type = "group",
      childGroups = "select",
      name = L["Module Settings"],
      desc = L["Configuration settings for each module"],
      args = { },
      plugins = { },
      order = 3,
    },
  },
  plugins = { }
}
ReAction.options = options

local SelectBar, DestroyBar, InitializeBars, TearDownBars, DeepCopy, CallModuleMethod, SlashHandler
do
  local pcall = pcall
  local geterrorhandler = geterrorhandler
  local self = ReAction
  local inited = false

  function SelectBar(x)
    local bar, name
    if type(x) == "string" then
      name = x
      bar = self:GetBar(name)
    else
      for k,v in pairs(bars) do
        if v == x then
          name = k
          bar = x
        end
      end
    end
    return bar, name
  end

  function DestroyBar(x)
    local bar, name = SelectBar(x)
    if bar and name then
      bars[name] = nil
      callbacks:Fire("OnDestroyBar", bar, name)
      bar:Destroy()
    end
  end

  function InitializeBars()
    if not inited then
      for name, config in pairs(self.db.profile.bars) do
        if config then
          self:CreateBar(name, config)
        end
      end
      -- re-anchor in case anchor order does not match init order
      for name, bar in pairs(bars) do
        bar:ApplyAnchor()
      end
      inited = true
    end
  end

  function TearDownBars()
    for name, bar in pairs(bars) do
      if bar then
        bars[name] = DestroyBar(bar)
      end
    end
    inited = false
  end

  function DeepCopy(x)
    if type(x) ~= "table" then
      return x
    end
    local r = {}
    for k,v in pairs(x) do
      r[k] = DeepCopy(v)
    end
    return r
  end

  function CallModuleMethod(modulename, method, ...)
    local m = self:GetModule(modulename,true)
    if not m then
      LoadAddOn(("ReAction_%s"):format(modulename))
      m = self:GetModule(modulename,true)
      if m then
        dbprint(("succesfully loaded LOD module: %s"):format(modulename))
      end
    end
    if m then
      if type(m) == "table" and type(m[method]) == "function" then
        m[method](m,...)
      else
        dbprint(("Bad call '%s' to %s module"):format(tostring(method),modulename));
      end
    else
      self:Print(("Module '%s' not found"):format(tostring(modulename)))
    end
  end

  function SlashHandler(option)
    if option == "config" then
      self:ShowConfig()
    elseif option == "edit" then
      self:ShowEditor()
    elseif option == "unlock" then
      self:SetConfigMode(true)
    elseif option == "lock" then
      self:SetConfigMode(false)
    else
      self:Print(("%3.1f.%d"):format(version,self.revision))
      self:Print("/rxn config")
      self:Print("/rxn edit")
      self:Print("/rxn lock")
      self:Print("/rxn unlock")
    end
  end
end


------ HANDLERS ------
function ReAction:OnInitialize()
  self.db = LibStub("AceDB-3.0"):New("ReAction_DB", 
    { 
      profile = {
        bars = { },
        defaultBar = { }
      }
    }
    -- default profile is character-specific
  )
  self.db.RegisterCallback(self,"OnProfileChanged")
  self.db.RegisterCallback(self,"OnProfileReset","OnProfileChanged")

  options.args.profile = LibStub("AceDBOptions-3.0"):GetOptionsTable(self.db)

  self:RegisterChatCommand("reaction", SlashHandler)
  self:RegisterChatCommand("rxn", SlashHandler)
  self:RegisterEvent("PLAYER_REGEN_DISABLED")
end

function ReAction:OnEnable()
  InitializeBars()
end

function ReAction:OnDisable()
  TearDownBars()
end

function ReAction:OnProfileChanged()
  TearDownBars()
  InitializeBars()
end

function ReAction:PLAYER_REGEN_DISABLED()
  if private.configMode == true then
    self:UserError(L["ReAction config mode disabled during combat."])
    self:SetConfigMode(false)
  end
end



------ API ------
function ReAction:UserError(msg)
  -- any user errors should be flashed to the UIErrorsFrame
  UIErrorsFrame:AddMessage(msg)
end

-- usage:
--  (1) ReAction:CreateBar(name, cfgTable)
--  (2) ReAction:CreateBar(name, "barType", [nRows], [nCols], [btnSize], [btnSpacing])
function ReAction:CreateBar(name, ...)
  local config = select(1,...)
  if config and type(config) ~= "table" then
    bartype = select(1,...)
    if type(bartype) ~= "string" then
      error("ReAction:CreateBar() - first argument must be a config table or a default config type string")
    end
    config = defaultBarConfig[bartype]
    if not config then
      error(("ReAction:CreateBar() - unknown bar type '%s'"):format(bartype))
    end
    config = DeepCopy(config)
    config.btnRows    = select(2,...) or config.btnRows    or 1
    config.btnColumns = select(3,...) or config.btnColumns or 12
    config.btnWidth   = select(4,...) or config.btnWidth   or 36
    config.btnHeight  = select(4,...) or config.btnHeight  or 36
    config.spacing    = select(5,...) or config.spacing    or 3
    config.width      = config.width or config.btnColumns*(config.btnWidth + config.spacing) + 1
    config.height     = config.height or config.btnRows*(config.btnHeight + config.spacing) + 1
    config.anchor     = config.anchor or "BOTTOM"
    config.anchorTo   = config.anchorTo or "UIParent"
    config.relativePoint = config.relativePoint or "BOTTOM"
    config.y          = config.y or 200
    config.x          = config.x or 0
  end
  local profile = self.db.profile
  config = config or DeepCopy(profile.defaultBar)
  prefix = prefix or L["Bar "]
  if not name then
    i = 1
    repeat
      name = prefix..i
      i = i + 1
    until bars[name] == nil
  end
  profile.bars[name] = profile.bars[name] or config
  local bar = self.Bar:new( name, profile.bars[name] )  -- ReAction.Bar defined in Bar.lua
  bars[name] = bar
  callbacks:Fire("OnCreateBar", bar, name)
  if private.configMode then
    bar:ShowControls(true)
  end

  return bar
end

function ReAction:EraseBar(x)
  local bar, name = SelectBar(x)
  if bar and name then
    callbacks:Fire("OnEraseBar", bar, name)
    DestroyBar(bar)
    self.db.profile.bars[name] = nil
  end
end

function ReAction:GetBar(name)
  return bars[name]
end

function ReAction:IterateBars()
  return pairs(bars)
end

function ReAction:RenameBar(x, newname)
  local bar, name = SelectBar(x)
  if type(newname) ~= "string" then
    error("ReAction:RenameBar() - second argument must be a string")
  end
  if bar and name and #newname > 0 then
    if bars[newname] then
      self:UserError(("%s ('%s')"):format(L["ReAction: name already in use"],newname))
    else
      bars[newname], bars[name] = bars[name], nil
      bar:SetName(newname or "")
      local cfg = self.db.profile.bars
      cfg[newname], cfg[name] = cfg[name], nil
      callbacks:Fire("OnRenameBar", bar, name, newname)
    end
  end
end

function ReAction:RefreshBar(x)
  local bar, name = SelectBar(x)
  if bar and name then
    callbacks:Fire("OnRefreshBar", bar, name)
  end
end

function ReAction:RegisterBarType( name, config, isDefaultChoice )
  defaultBarConfig[name] = config
  if isDefaultChoice then
    defaultBarConfigChoice = name
  end
  self:RefreshOptions()
end

function ReAction:UnregisterBarType( name )
  defaultBarConfig[name] = nil
  if private.defaultBarConfigChoice == name then
    private.defaultBarConfigChoice = nil
  end
  self:RefreshOptions()
end

function ReAction:IterateBarTypes()
  return pairs(defaultBarConfig)
end

function ReAction:GetBarTypeConfig(name)
  if name then
    return defaultBarConfig[name]
  end
end

function ReAction:GetBarTypeOptions( fill )
  fill = fill or { }
  for k in self:IterateBarTypes() do
    fill[k] = k
  end
  return fill
end

function ReAction:GetDefaultBarType()
  return private.defaultBarConfigChoice
end

function ReAction:RegisterOptions(module, opts, global)
  options.args[global and "global" or "module"].plugins[module:GetName()] = opts
  self:RefreshOptions()
end

function ReAction:RefreshOptions()
  callbacks:Fire("OnOptionsRefreshed")
end

-- 
-- In addition to global and general module options, options tables 
-- must be generated dynamically for each bar.
--
-- 'func' should be a function or a method string.
-- The function or method will be passed the bar as its parameter.
-- (methods will of course get the module as the first 'self' parameter)
-- 
-- A generator can be unregistered by passing a nil func.
--
function ReAction:RegisterBarOptionGenerator( module, func )
  if not module or type(module) ~= "table" then -- doesn't need to be a proper module, strictly
    error("ReAction:RegisterBarOptionGenerator() : Invalid module")
  end
  if type(func) == "string" then
    if not module[func] then
      error(("ReAction:RegisterBarOptionGenerator() : Invalid method '%s'"):format(func))
    end
  elseif func and type(func) ~= "function" then
    error("ReAction:RegisterBarOptionGenerator() : Invalid function")
  end
  barOptionGenerators[module] = func
  callbacks:Fire("OnBarOptionGeneratorRegistered", module, func)
end

-- builds a table suitable for use as an AceConfig3 group 'plugins' sub-table
function ReAction:GenerateBarOptionsTable( bar )
  local opts = { }
  for module, func in pairs(barOptionGenerators) do
    local success, r
    if type(func) == "string" then
      success, r = pcall(module[func], module, bar)
    else
      success, r = pcall(func, bar)
    end
    if success then
      opts[module:GetName()] = { [module:GetName()] = r }
    else
      geterrorhandler()(r)
    end
  end
  return opts
end

function ReAction:SetConfigMode( mode )
  private.configMode = mode
  callbacks:Fire("OnConfigModeChanged", mode)
end

function ReAction:GetConfigMode()
  return private.configMode
end

function ReAction:ShowConfig()
  CallModuleMethod("ConfigUI","OpenConfig")
end

function ReAction:ShowEditor(bar)
  CallModuleMethod("ConfigUI","LaunchBarEditor",bar)
end