diff Bar.lua @ 75:06cd74bdc7da

- Cleaned up Bar interface - Move all attribute setting from Bar into State - Separated Moonkin and Tree of Life - Removed PossessBar module - Added some infrastructure for paged/mind control support to Action
author Flick <flickerstreak@gmail.com>
date Mon, 16 Jun 2008 18:46:08 +0000
parents dd01feae0d89
children da8ba8783924
line wrap: on
line diff
--- a/Bar.lua	Tue Jun 10 22:25:15 2008 +0000
+++ b/Bar.lua	Mon Jun 16 18:46:08 2008 +0000
@@ -3,84 +3,91 @@
 local _G = _G
 local CreateFrame = CreateFrame
 local floor = math.floor
+local fmod = math.fmod
+local format = string.format
 local SecureStateHeader_Refresh = SecureStateHeader_Refresh
 
 
-
 -- update ReAction revision if this file is newer
 local revision = tonumber(("$Revision$"):match("%d+"))
 if revision > ReAction.revision then
   ReAction.revision = revision
 end
 
+
 ------ BAR CLASS ------
 local Bar = { _classID = {} }
 
 local function Constructor( self, name, config )
-  self.name, self.config = name, config
-  self.buttons = setmetatable({},{__mode="k"})
-
   if type(config) ~= "table" then
     error("ReAction.Bar: config table required")
   end
+  config.width = config.width or 480
+  config.height = config.height or 40
+
+  self.name, self.config = name, config
+  self.buttons = setmetatable({},{__mode="k"})
+  self.statedrivers = { }
+  self.keybinds = { }
 
   local parent = config.parent and (ReAction:GetBar(config.parent) or _G[config.parent]) or UIParent
-  local f = CreateFrame("Frame",nil,parent,"SecureStateHeaderTemplate")
+  local f = CreateFrame("Button",name and format("ReAction-%s",name),parent,"SecureStateHeaderTemplate, SecureActionButtonTemplate")
+
+  -- The frame itself is read-only
+  function self:GetFrame()
+    return f
+  end
+
+  -- The bar itself is also a Button derived from SecureActionButtonTemplate, so it has an OnClick handler
+  -- which we can use as a virtual button for keybinds, which will send attribute-value changes to itself.
+  -- However, we don't ever want the user to be able to click it directly.
+  f:EnableMouse(false)
+  f:SetAttribute("type","attribute")
   f:SetFrameStrata("MEDIUM")
-  config.width = config.width or 480
-  config.height = config.height or 40
   f:SetWidth(config.width)
   f:SetWidth(config.height)
+  f:Show()
 
+  self:ApplyAnchor()
   ReAction.RegisterCallback(self, "OnConfigModeChanged")
-
-  self.frame = f
-  self:ApplyAnchor()
-  f:Show()
-  self:RefreshLayout()
 end
 
 function Bar:Destroy()
-  local f = self.frame
+  local f = self:GetFrame()
   f:UnregisterAllEvents()
   f:Hide()
   f:SetParent(UIParent)
   f:ClearAllPoints()
   ReAction.UnregisterAllCallbacks(self)
-  if self.statedriver then
-    UnregisterStateDriver(f, "reaction")
+  for driver in pairs(self.statedrivers) do
+    UnregisterStateDriver(f, driver)
   end
   self.labelString = nil
   self.controlFrame = nil
-  self.frame = nil
   self.config = nil
 end
 
 function Bar:OnConfigModeChanged(event, mode)
-  self:ShowControls(mode) -- ShowControls() defined in Overlay.lua
-end
-
-function Bar:RefreshLayout()
-  ReAction:RefreshBar(self)
+  self:ShowControls(mode) -- Bar:ShowControls() defined in Overlay.lua
 end
 
 function Bar:ApplyAnchor()
-  local f, config = self.frame, self.config
+  local f, config = self:GetFrame(), self.config
   f:SetWidth(config.width)
   f:SetHeight(config.height)
-  local anchor = config.anchor
+  local point  = config.point
   f:ClearAllPoints()
-  if anchor then
-    local anchorTo = f:GetParent()
-    if config.anchorTo then
-      local bar = ReAction:GetBar(config.anchorTo)
+  if point then
+    local anchor = f:GetParent()
+    if config.anchor then
+      local bar = ReAction:GetBar(config.anchor)
       if bar then
-        anchorTo = bar:GetFrame()
+        anchor = bar:GetFrame()
       else
-        anchorTo = _G[config.anchorTo]
+        anchor = _G[config.anchor]
       end
     end
-    f:SetPoint(anchor, anchorTo or f:GetParent(), config.relativePoint, config.x or 0, config.y or 0)
+    f:SetPoint(point, anchor or f:GetParent(), config.relpoint, config.x or 0, config.y or 0)
   else
     f:SetPoint("CENTER")
   end
@@ -88,9 +95,9 @@
 
 function Bar:SetAnchor(point, frame, relativePoint, x, y)
   local c = self.config
-  c.anchor = point or c.anchor
-  c.anchorTo = frame and frame:GetName() or c.anchorTo
-  c.relativePoint = relativePoint or c.relativePoint
+  c.point = point or c.point
+  c.anchor = frame and frame:GetName() or c.anchor
+  c.relpoint = relativePoint or c.relpoint
   c.x = x or c.x
   c.y = y or c.y
   self:ApplyAnchor()
@@ -98,20 +105,20 @@
 
 function Bar:GetAnchor()
   local c = self.config
-  return (c.anchor or "CENTER"), (c.anchorTo or self.frame:GetParent():GetName()), (c.relativePoint or c.anchor or "CENTER"), (c.x or 0), (c.y or 0)
-end
-
-function Bar:GetFrame()
-  return self.frame
+  return (c.point or "CENTER"), (c.anchor or self:GetFrame():GetParent():GetName()), (c.relpoint or c.point or "CENTER"), (c.x or 0), (c.y or 0)
 end
 
 function Bar:GetSize()
-  return self.frame:GetWidth() or 200, self.frame:GetHeight() or 200
+  local f = self:GetFrame()
+  return f:GetWidth(), f:GetHeight()
 end
 
 function Bar:SetSize(w,h)
   self.config.width = w
   self.config.height = h
+  local f = self:GetFrame()
+  f:SetWidth(w)
+  f:SetHeight(h)
 end
 
 function Bar:GetButtonSize()
@@ -126,6 +133,7 @@
     self.config.btnWidth = w
     self.config.btnHeight = h
   end
+  ReAction:RefreshBar(self)
 end
 
 function Bar:GetButtonGrid()
@@ -143,188 +151,128 @@
     cfg.btnColumns = c
     cfg.spacing = s
   end
+  ReAction:RefreshBar(self)
 end
 
 function Bar:GetName()
   return self.name
 end
 
+-- only ReAction:RenameBar() should call this function
 function Bar:SetName(name)
   self.name = name
+  -- controlLabelString is defined in Overlay.lua
   if self.controlLabelString then
     self.controlLabelString:SetText(self.name)
   end
 end
 
-function Bar:PlaceButton(f, idx, baseW, baseH)
+function Bar:AddButton(idx, button)
+  self.buttons[button] = idx
+  SecureStateHeader_Refresh(self:GetFrame())
+end
+
+function Bar:RemoveButton(button)
+  self.buttons[button] = nil
+end
+
+function Bar:IterateButtons() -- iterator returns button, idx
+  return pairs(self.buttons)
+end
+
+function Bar:PlaceButton(button, baseW, baseH)
+  local idx = self.buttons[button]
+  if not idx then return end
   local r, c, s = self:GetButtonGrid()
   local bh, bw = self:GetButtonSize()
-  local row, col = floor((idx-1)/c), mod((idx-1),c) -- zero-based
+  local row, col = floor((idx-1)/c), fmod((idx-1),c) -- zero-based
   local x, y = col*bw + (col+0.5)*s, row*bh + (row+0.5)*s
   local scale = bw/baseW
+  local f = button:GetFrame()
 
   f:ClearAllPoints()
   f:SetPoint("TOPLEFT",x/scale,-y/scale)
   f:SetScale(scale)
-  self.buttons[f] = true
 end
 
+-- Creates (or updates) a named binding which binds a key press to a call to SetAttribute()
+-- pass a nil key to unbind
+function Bar:SetAttributeBinding( name, key, attribute, value )
+  if not name then
+    error("usage - Bar:SetAttributeBinding(name [, key, attribute, value]")
+  end
+  local f = self:GetFrame()
 
--- multi-state functions --
-function Bar:GetNumPages()
-  return self.config.nPages or 1
+  -- clear the old binding, if any
+  if self.keybinds[name] then
+    SetOverrideBinding(f, false, self.keybinds[name], nil)
+  end
+  if key then
+    f:SetAttribute(format("attribute-name-%s",name), attribute)
+    f:SetAttribute(format("attribute-value-%s",name), value)
+    SetOverrideBindingClick(f, false, key, f:GetName(), name) -- binding name is the virtual mouse button
+  end
+  self.keybinds[name] = key
 end
 
-  --
-  -- 'rule' is a rule-string to pass to RegisterStateDriver
-  -- 'states' is a { ["statename"] = <don't care> } table of all state names
-  -- 'keybinds' is a { ["statename"] = keybind } table of all keybound states
-  --
-function Bar:SetStateDriver( rule, states, keybinds )
-  local f = self.frame
-  local kbprefix = ""
-  do
-    local tmp = { }
-    for s, k in pairs(keybinds) do
-      if k and #k > 0 then -- filter out false table entries
-        -- if in a keybound state, set the stack to the new state but stay in the keybound state.
-        -- use $s as a placeholder for the current state, it will be gsub()'d in later
-        table.insert(tmp,("%s:$s set() %s"):format(s,s))
+-- Sets up a state driver 'name' for the bar, using the provided 'rule'
+-- Also sets attributes 'statemap-<name>-<key>'=<value> for each entry in the passed map
+-- if 'rule' is nil or an empty string, the driver is unregistered.
+function Bar:SetStateDriver( name, rule, map )
+  local f = self:GetFrame()
+  if rule and #rule > 0 then
+    if map then
+      for key, value in pairs(map) do
+        f:SetAttribute( format("statemap-%s-%s",name,key), value )
       end
     end
-    table.insert(tmp,kbprefix)  -- to get a trailing ';' if the table is not empty
-    kbprefix = table.concat(tmp,";")
-  end
-  for state in pairs(states) do
-    -- For all states: if in a keybound state, stay there (with stack manipulation, see above).
-    -- Otherwise, go to the state
-    f:SetAttribute(("statemap-reaction-%s"):format(state),("%s%s"):format(kbprefix:gsub("%$s",state),state))
-
-    local binding = keybinds[state]
-    self:SetStateKeybind(binding, state) -- set the binding even if nil, to clear it unconditionally
-    if binding then
-      -- for key bindings, use the state-stack to toggle between the last state and the keybound state
-      -- use a different 'virtual state' passed to attribute 'reaction-state' for key bindings, "<state>_binding"
-      f:SetAttribute(("statemap-reaction-%s_binding"):format(state), ("%s:pop();*:set(%s)"):format(state,state))
-    end
-  end
-
-  if rule and #rule > 0 then
-    self.stateDriver = true
-    RegisterStateDriver(f, "reaction", rule)
-  elseif self.statedriver then
-    self.statedriver = false
-    UnregisterStateDriver(f, "reaction")
+    RegisterStateDriver(f, name, rule)
+    self.statedrivers[name] = true
+  elseif self.statedrivers[name] then
+    UnregisterStateDriver(f, name)
+    self.statedrivers[name] = nil
   end
 end
 
-function Bar:SetHideStates(s)
-  for f in pairs(self.buttons) do
-    if f:GetParent() == self.frame then
-      f:SetAttribute("hidestates",s)
+-- Set an attribute on the frame (or its buttons if 'doButtons' = true)
+-- Either or both 'map' and 'default' can be passed:
+--   - If 'map' is omitted, then 'default' is set to the attribute.
+--   - If 'map' is provided, then it is interpreted as an unordered
+--     table of the form { ["statename"] = ["value"] }, and will be
+--     converted into a SecureStateHeaderTemplate style state-parsed
+--     string, e.g. "<state1>:<value1>;<state2>:<value2>". If 'default'
+--     is also provided, then its value will be converted to a string
+--     and appended.
+function Bar:SetStateAttribute( attribute, map, default, doButtons )
+  local value = default
+  if map then
+    local tmp = { }
+    for state, value in pairs(map) do
+      table.insert(tmp, format("%s:%s",tostring(state),tostring(value)))
     end
+    if default then
+      table.insert(tmp, tostring(default))
+    end
+    value = table.concat(tmp,";")
   end
-  SecureStateHeader_Refresh(self.frame)
-end
-
-function Bar:SetStateKeybind(key, state, defaultstate)
-  -- Lazily create a tiny offscreen button which sends "<state>_binding" values to the
-  -- bar frame's state-reaction attribute, by using an override binding to generate a 
-  -- click on the button with a virtual mouse button "state". 
-  -- This gets around making the bar itself a clickable button, which is not desirable
-  local f = self.statebuttonframe
-  if key then
-    if not f then
-      f = CreateFrame("Button",self:GetName().."_statebutton",self.frame,"SecureActionButtonTemplate")
-      f:SetPoint("BOTTOMRIGHT",UIParent,"TOPLEFT")
-      f:SetWidth(1)
-      f:SetHeight(1)
-      f:SetAttribute("type*","attribute")
-      f:SetAttribute("attribute-name*","state-reaction")
-      f:SetAttribute("attribute-frame*",self.frame)
-      f:Show()
-      f.bindings = { }
-      self.statebuttonframe = f
-    end
-    f:SetAttribute(("attribute-value-%s"):format(state),("%s_binding"):format(state))
-      -- clear the old binding, if any, for this state
-    if f.bindings[state] then
-      SetOverrideBinding(self.frame, false, f.bindings[state], nil)
-    end
-    SetOverrideBindingClick(self.frame, false, key, f:GetName(), state) -- the state name is used as the virtual button
-    f.bindings[state] = key
-  elseif f then
-    key = f.bindings[state]
-    if key then
-      SetOverrideBinding(self.frame, false, key, nil)
-      f.bindings[state] = nil
-    end
-  end
-end
-
-function Bar:SetStatePageMap(state, map)  -- map is a { ["statename"] = pagenumber } table
-  local f = self.frame
-  local tmp = { }
-  for s, p in pairs(map) do
-    table.insert(tmp, ("%s:page%d"):format(s,p))
-  end
-  local spec = table.concat(tmp,";")
-  local current = f:GetAttribute("statebutton")
-  if spec ~= f:GetAttribute("statebutton") then
-    f:SetAttribute("statebutton", spec)
-  end
-  SecureStateHeader_Refresh(f)
-end
-
-function Bar:SetStateKeybindOverrideMap(states) -- 'states' is an array of state-names that should have keybind overrides enabled
-  local f = self.frame
-  for i = 1, #states do
-    local s = states[i]
-    states[i] = ("%s:%s"):format(s,s)
-  end
-  table.insert(states,"_defaultbindings")
-  f:SetAttribute("statebindings",table.concat(states,";"))
-  SecureStateHeader_Refresh(f)
-  for b in pairs(self.buttons) do
-    -- TODO: signal child frames that they should maintain multiple bindings
-  end
-end
-
-local _ofskeys = { "point", "relpoint", "x", "y" }
-function Bar:SetStateAnchorMap( map ) -- 'map' is a { ["statename"] = { point=point, relpoint=relpoint, x=x, y=y } } table
-  local f = self.frame
-  local c = self.config
-  local default = { point = c.anchor, relpoint = c.relativePoint, x = c.x, y = c.y }
-  for _, key in pairs(_ofskeys) do
-    local t = { }
-    for state, info in pairs(map) do
-      if info[key] then
-        table.insert(t, ("%s:%s"):format(state, info[key]))
+  if doButtons then
+    for b in pairs(self.buttons) do
+      local f = b.GetFrame and b:GetFrame()
+      if f then
+        f:SetAttribute(attribute, value)
       end
     end
-    if #t > 0 and default[key] then table.insert(t, tostring(default[key])) end
-    f:SetAttribute(("headofs%s"):format(key), table.concat(t,";") or "")
+  else
+    self:GetFrame():SetAttribute(attribute, value)
   end
-  SecureStateHeader_Refresh(f)
+  SecureStateHeader_Refresh(self:GetFrame())
 end
 
-function Bar:SetStateScaleMap( map ) -- 'map' is a { ["statename"] = scalevalue } table
-  local f = self.frame
-  local t = { }
-  for state, scale in pairs(map) do
-    table.insert( t, ("%s:%s"):format(state,scale) )
-  end
-  if #t > 0 then table.insert(t, "1.0") end
-  f:SetAttribute("headscale",table.concat(t,";") or "")
-  SecureStateHeader_Refresh(f)
-end
-
-
 
 ------ Export as a class-factory ------
 ReAction.Bar = {
   prototype = Bar,
-  new = function(self, ...)
+  New = function(self, ...)
     local x = { }
     for k,v in pairs(Bar) do
       x[k] = v