diff ReAction.lua @ 63:768be7eb22a0

Converted several ReAction APIs to event-driven model instead of 'call-method-on-all-modules' model. Cleaned up a number of other architectural issues.
author Flick <flickerstreak@gmail.com>
date Thu, 22 May 2008 22:02:08 +0000
parents 2ee41dcd673f
children 06cd74bdc7da
line wrap: on
line diff
--- a/ReAction.lua	Tue May 13 16:42:52 2008 +0000
+++ b/ReAction.lua	Thu May 22 22:02:08 2008 +0000
@@ -1,13 +1,37 @@
--- ReAction.lua
--- See modules/ReAction_ModuleTemplate for Module API listing
--- See Bar.lua for Bar object listing
+--[[
+  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.version = GetAddOnMetadata("ReAction","Version")
 ReAction.revision = tonumber(("$Revision$"):match("%d+"))
 
 ------ GLOBALS ------
@@ -20,29 +44,78 @@
   dbprint = function(msg)
     DEFAULT_CHAT_FRAME:AddMessage(msg)
   end
-  --seterrorhandler(dbprint)
 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 SelectBar, DestroyBar, InitializeBars, TearDownBars, DeepCopy, SafeCall, CheckMethod, SlashHandler
+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
 
-  SelectBar = function(x)
+  function SelectBar(x)
     local bar, name
     if type(x) == "string" then
       name = x
-      bar = ReAction:GetBar(name)
+      bar = self:GetBar(name)
     else
-      for k,v in pairs(ReAction.bars) do
+      for k,v in pairs(bars) do
         if v == x then
           name = k
           bar = x
@@ -52,37 +125,40 @@
     return bar, name
   end
 
-  DestroyBar = function(x)
+  function DestroyBar(x)
     local bar, name = SelectBar(x)
-    if name and bar then
-      ReAction.bars[name] = nil
-      ReAction:CallMethodOnAllModules("RemoveFromBar", bar)
+    if bar and name then
+      bars[name] = nil
+      callbacks:Fire("OnDestroyBar", bar, name)
       bar:Destroy()
     end
   end
 
-  InitializeBars = function ()
-    if not(ReAction.inited) then
-      for name, config in pairs(ReAction.db.profile.bars) do
+  function InitializeBars()
+    if not inited then
+      for name, config in pairs(self.db.profile.bars) do
         if config then
-          ReAction:CreateBar(name, config)
+          self:CreateBar(name, config)
         end
       end
-      ReAction:CallMethodOnAllBars("ApplyAnchor") -- re-anchor in the case of oddball ordering
-      ReAction.inited = true
+      -- 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
 
-  TearDownBars = function()
-    for name, bar in pairs(ReAction.bars) do
+  function TearDownBars()
+    for name, bar in pairs(bars) do
       if bar then
-        ReAction.bars[name] = DestroyBar(bar)
+        bars[name] = DestroyBar(bar)
       end
     end
-    ReAction.inited = false
+    inited = false
   end
 
-  DeepCopy = function(x)
+  function DeepCopy(x)
     if type(x) ~= "table" then
       return x
     end
@@ -93,39 +169,41 @@
     return r
   end
 
-  SafeCall = function(f, ...)
-    if f then
-      local success, err = pcall(f,...)
-      if not success then
-        geterrorhandler()(err)
+  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
 
-  CheckMethod = function(m)
-    if type(m) == "function" then
-      return m
-    end
-    if type(m) ~= "string" then
-      error("Invalid method")
-    end
-  end
-
-  SlashHandler = function(option)
+  function SlashHandler(option)
     if option == "config" then
-      ReAction:ShowConfig()
+      self:ShowConfig()
     elseif option == "edit" then
-      ReAction:ShowEditor()
+      self:ShowEditor()
     elseif option == "unlock" then
-      ReAction:SetConfigMode(true)
+      self:SetConfigMode(true)
     elseif option == "lock" then
-      ReAction:SetConfigMode(false)
+      self:SetConfigMode(false)
     else
-      ReAction:Print(("%3.1f.%d"):format(ReAction.version,ReAction.revision))
-      ReAction:Print("/reaction config")
-      ReAction:Print("/reaction edit")
-      ReAction:Print("/reaction lock")
-      ReAction:Print("/reaction unlock")
+      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
@@ -144,57 +222,12 @@
   )
   self.db.RegisterCallback(self,"OnProfileChanged")
   self.db.RegisterCallback(self,"OnProfileReset","OnProfileChanged")
-  self.callbacks = LibStub("CallbackHandler-1.0"):New(self)
+
+  options.args.profile = LibStub("AceDBOptions-3.0"):GetOptionsTable(self.db)
+
   self:RegisterChatCommand("reaction", SlashHandler)
   self:RegisterChatCommand("rxn", SlashHandler)
   self:RegisterEvent("PLAYER_REGEN_DISABLED")
-
-  self.bars = {}
-  self.defaultBarConfig = {}
-
-  self.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",
-            handler  = module,
-            name     = L["Unlock Bars"],
-            desc     = L["Unlock bars for dragging and resizing with the mouse"],
-            get      = function() return self.configMode end,
-            set      = function(info, value) self: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,
-      },
-      profile = LibStub("AceDBOptions-3.0"):GetOptionsTable(self.db)
-    },
-    plugins = { }
-  }
-
 end
 
 function ReAction:OnEnable()
@@ -210,29 +243,9 @@
   InitializeBars()
 end
 
-function ReAction:OnModuleEnable(module)
-  if module.ApplyToBar then
-    for _, b in pairs(bars) do
-      if b then
-        module:ApplyToBar(b)
-      end
-    end
-  end
-end
-
-function ReAction:OnModuleDisable(module)
-  if module.RemoveFromBar then
-    for _, b in pairs(bars) do
-      if b then
-        module:RemoveFromBar(b)
-      end
-    end
-  end
-end
-
 function ReAction:PLAYER_REGEN_DISABLED()
-  if self.configMode == true then
-    UIErrorsFrame:AddMessage(L["ReAction config mode disabled during combat."])
+  if private.configMode == true then
+    self:UserError(L["ReAction config mode disabled during combat."])
     self:SetConfigMode(false)
   end
 end
@@ -245,45 +258,9 @@
   UIErrorsFrame:AddMessage(msg)
 end
 
-function ReAction:CallMethodOnAllModules(method, ...)
-  local m = CheckMethod(method)
-  for _, x in self:IterateModules() do
-    if x then
-      SafeCall(m or x[method], x, ...)
-    end
-  end
-end
-
-function ReAction:CallMethodOnAllBars(method,...)
-  local m = CheckMethod(method)
-  for _, x in pairs(self.bars) do
-    if x then
-      SafeCall(m or x[method], x, ...)
-    end
-  end
-end
-
-function ReAction: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
-
-
+-- 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
@@ -291,7 +268,7 @@
     if type(bartype) ~= "string" then
       error("ReAction:CreateBar() - first argument must be a config table or a default config type string")
     end
-    config = self.defaultBarConfig[bartype]
+    config = defaultBarConfig[bartype]
     if not config then
       error(("ReAction:CreateBar() - unknown bar type '%s'"):format(bartype))
     end
@@ -317,14 +294,13 @@
     repeat
       name = prefix..i
       i = i + 1
-    until self.bars[name] == nil
+    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
-  self:CallMethodOnAllModules("ApplyToBar", bar)
-  self.bars[name] = bar
-  self.callbacks:Fire("OnCreateBar", bar)
-  if self.configMode then
+  bars[name] = bar
+  callbacks:Fire("OnCreateBar", bar, name)
+  if private.configMode then
     bar:ShowControls(true)
   end
 
@@ -333,69 +309,150 @@
 
 function ReAction:EraseBar(x)
   local bar, name = SelectBar(x)
-  if name and bar then
+  if bar and name then
+    callbacks:Fire("OnEraseBar", bar, name)
     DestroyBar(bar)
     self.db.profile.bars[name] = nil
-    self:CallMethodOnAllModules("EraseBarConfig", name)
-    self.callbacks:Fire("OnEraseBar",name)
   end
 end
 
 function ReAction:GetBar(name)
-  return self.bars[name]
+  return bars[name]
+end
+
+function ReAction:IterateBars()
+  return pairs(bars)
 end
 
 function ReAction:RenameBar(x, newname)
   local bar, name = SelectBar(x)
-  if bar and name and newname then
-    if self.bars[newname] then
-      UIErrorsFrame:AddMessage(("%s ('%s')"):format(L["ReAction: name already in use"],newname))
+  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
-      self.bars[newname] = self.bars[name]
-      self.bars[name] = nil
+      bars[newname], bars[name] = bars[name], nil
       bar:SetName(newname or "")
       local cfg = self.db.profile.bars
       cfg[newname], cfg[name] = cfg[name], nil
-      self:CallMethodOnAllModules("RenameBarConfig", name, newname)
-      self.callbacks:Fire("OnRenameBar", name, newname)
+      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 )
-  self.defaultBarConfig[name] = config
+  defaultBarConfig[name] = config
   if isDefaultChoice then
-    self.defaultBarConfigChoice = name
+    defaultBarConfigChoice = name
   end
   self:RefreshOptions()
 end
 
 function ReAction:UnregisterBarType( name )
-  self.defaultBarConfig[name] = nil
-  if self.defaultBarConfigChoice == name then
-    self.defaultBarConfigChoice = nil
+  defaultBarConfig[name] = nil
+  if private.defaultBarConfigChoice == name then
+    private.defaultBarConfigChoice = nil
   end
   self:RefreshOptions()
 end
 
-function ReAction:RegisterOptions(module, options, global)
-  self.options.args[global and "global" or "module"].plugins[module:GetName()] = options
+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()
-  self.callbacks:Fire("OnOptionsRefreshed")
+  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 )
-  self:CallMethodOnAllBars("ShowControls",mode)
-  self:CallMethodOnAllModules("ApplyConfigMode",mode,self.bars)
-  self.configMode = mode
+  private.configMode = mode
+  callbacks:Fire("OnConfigModeChanged", mode)
+end
+
+function ReAction:GetConfigMode()
+  return private.configMode
 end
 
 function ReAction:ShowConfig()
-  self:CallModuleMethod("ConfigUI","OpenConfig")
+  CallModuleMethod("ConfigUI","OpenConfig")
 end
 
-function ReAction:ShowEditor()
-  self:CallModuleMethod("ConfigUI","LaunchBarEditor")
+function ReAction:ShowEditor(bar)
+  CallModuleMethod("ConfigUI","LaunchBarEditor",bar)
 end