diff Bar.lua @ 245:65f2805957a0

No real reason to store some of the code in a subdirectory.
author Flick
date Sat, 26 Mar 2011 12:35:08 -0700
parents classes/Bar.lua@b56cff349bd6
children 36a29870bf34
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Bar.lua	Sat Mar 26 12:35:08 2011 -0700
@@ -0,0 +1,854 @@
+local addonName, addonTable = ...
+local ReAction = addonTable.ReAction
+local L = ReAction.L
+local LKB = ReAction.LKB
+local _G = _G
+local CreateFrame = CreateFrame
+local floor = math.floor
+local fmod = math.fmod
+local format = string.format
+local tfetch = addonTable.tfetch
+local tbuild = addonTable.tbuild
+local fieldsort = addonTable.fieldsort
+
+local LSG = LibStub("ReAction-LibShowActionGrid-1.0")
+
+---- Secure snippets ----
+local _reaction_init = 
+[[
+  anchorKeys = newtable("point","relPoint","x","y")
+
+  state = nil
+  set_state = nil
+  state_override = nil
+  unit_exists = nil
+
+  showAll = false
+  hidden = false
+
+  defaultAlpha = 1.0
+  defaultScale = 1.0
+  defaultAnchor = newtable()
+
+  activeStates = newtable()
+  settings = newtable()
+  extensions = newtable()
+]]
+
+local _reaction_refresh = 
+[[
+  local oldState = state
+  state = state_override or set_state or state
+
+  local hide = nil
+  if state then
+    local settings = settings[state]
+    if settings then
+      -- show/hide
+      hide = settings.hide
+      -- re-anchor
+      local old_anchor = activeStates.anchor
+      activeStates.anchor = settings.anchorEnable and state
+      if old_anchor ~= activeStates.anchor or not set_state then
+        if activeStates.anchor then
+          if settings.anchorPoint then
+            self:ClearAllPoints()
+            local f = self:GetAttribute("frameref-anchor-"..state)
+            if f then
+              self:SetPoint(settings.anchorPoint, f, settings.anchorRelPoint, settings.anchorX, settings.anchorY)
+            end
+          end
+        elseif defaultAnchor.point then
+          self:ClearAllPoints()
+          self:SetPoint(defaultAnchor.point, defaultAnchor.frame, 
+                        defaultAnchor.relPoint, defaultAnchor.x, defaultAnchor.y)
+        end
+      end
+      -- re-scale
+      local old_scale = activeStates.scale
+      activeStates.scale = settings.enableScale and state
+      if old_scale ~= activeStates.scale or not set_state then
+        self:SetScale(activeStates.scale and settings.scale or defaultScale)
+      end
+      -- alpha
+      local old_alpha = activeStates.alpha
+      activeStates.alpha = settings.enableAlpha and state
+      if old_alpha ~= activeStates.alpha or not set_state then
+        self:SetAlpha(activeStates.alpha and settings.alpha or defaultAlpha)
+      end
+    end
+  end
+
+  -- hide if state or unit_exists says to
+  hide = not showAll and (hide or unithide)
+  if hide ~= hidden then
+    hidden = hide
+    if hide then
+      self:Hide()
+    else
+      self:Show()
+    end
+  end
+
+  for _, attr in pairs(extensions) do
+    control:RunAttribute(attr)
+  end
+  
+  control:ChildUpdate()
+
+  if showAll then
+    control:CallMethod("UpdateHiddenLabel", state and settings[state] and settings[state].hide)
+  end
+
+  if oldState ~= state then
+    control:CallMethod("StateRefresh", state)
+  end
+]]
+
+local _onstate_reaction = -- function( self, stateid, newstate )
+[[
+  set_state = newstate
+]] .. _reaction_refresh
+
+local _onstate_showgrid = -- function( self, stateid, newstate )
+[[
+  control:ChildUpdate(stateid,newstate)
+  control:CallMethod("UpdateShowGrid")
+]]
+
+local _onstate_unitexists = -- function( self, stateid, newstate )
+[[
+  unithide = not newstate or newstate == "hide"
+]] .. _reaction_refresh
+
+local _onclick =  -- function( self, button, down )
+[[
+  if state_override == button then
+    state_override = nil -- toggle
+  else
+    state_override = button
+  end
+]] .. _reaction_refresh
+
+-- For reference
+-- the option field names must match the field names of the options table, below
+local stateProperties = { 
+  hide = true,
+  --keybindState = true, TODO: broken
+  anchorEnable = true,
+  anchorFrame = true,
+  anchorPoint = true,
+  anchorRelPoint = true,
+  anchorX = true,
+  anchorY = true,
+  enableScale = true,
+  scale = true,
+  enableAlpha = true,
+  alpha = true,
+}
+
+
+
+---- Bar class ----
+local Bar   = { }
+local frameList = { }
+
+ReAction.Bar = Bar -- export to ReAction
+
+function Bar:New( name, config, buttonClass )
+  if type(config) ~= "table" then
+    error("ReAction.Bar: config table required")
+  end
+
+  -- create new self
+  self = setmetatable( 
+    { 
+      config      = config,
+      name        = name,
+      buttons     = { },
+      buttonClass = buttonClass,
+      width       = config.width or 480,
+      height      = config.height or 40,
+    }, 
+    {__index = self} )
+  
+  -- The frame type is 'Button' in order to have an OnClick handler. However, the frame itself is
+  -- not mouse-clickable by the user.
+  local parent = config.parent and (ReAction:GetBar(config.parent) or _G[config.parent]) or UIParent
+  name = name and "ReAction-"..name
+  local f = name and frameList[name]
+  if not f then
+    f = CreateFrame("Button", name, parent, "SecureHandlerStateTemplate, SecureHandlerClickTemplate")
+    if name then
+      frameList[name] = f
+    end
+  end
+  f:SetFrameStrata("MEDIUM")
+  f:SetWidth(self.width)
+  f:SetHeight(self.height)
+  f:SetAlpha(config.alpha or 1.0)
+  f:Show()
+  f:EnableMouse(false)
+  f:SetClampedToScreen(true)
+  LSG:AddFrame(f)
+
+  -- secure handlers
+  f:Execute(_reaction_init)
+  f:SetAttribute("_onstate-reaction",   _onstate_reaction)
+  f:SetAttribute("_onstate-showgrid",   _onstate_showgrid)
+  f:SetAttribute("_onstate-unitexists", _onstate_unitexists)
+  f:SetAttribute("_onclick",            _onclick)
+
+  -- secure handler CallMethod()s
+  f.UpdateShowGrid    = function() self:UpdateShowGrid() end
+  f.StateRefresh      = function() self:RefreshControls() end
+  f.UpdateHiddenLabel = function(f,hidden) self:SetLabelSubtext(hidden and L["Hidden"]) end
+
+  -- Override the default frame accessor to provide strict read-only access
+  function self:GetFrame()
+    return f
+  end
+
+  self:ApplyAnchor()
+  self:SetConfigMode(ReAction:GetConfigMode())
+  self:SetKeybindMode(ReAction:GetKeybindMode())
+
+  if ReAction.LBF then
+    local g = ReAction.LBF:Group(L["ReAction"], self.name)
+    self.config.ButtonFacade = self.config.ButtonFacade or {
+      skinID = "Blizzard",
+      backdrop = true,
+      gloss = 0,
+      colors = {},
+    }
+    local c = self.config.ButtonFacade
+    g:Skin(c.skinID, c.gloss, c.backdrop, c.colors)
+    self.LBFGroup = g
+  end
+
+  ReAction.RegisterCallback(self, "OnConfigModeChanged")
+
+  buttonClass:SetupBar(self)
+  self:ApplyStates()
+
+  return self
+end
+
+function Bar:Destroy()
+  local f = self:GetFrame()
+  self:CleanupStates()
+  for idx, b in self:IterateButtons() do
+    b:Destroy()
+  end
+  f:UnregisterAllEvents()
+  self:ShowControls(false)
+  ReAction.UnregisterAllCallbacks(self)
+  LKB.UnregisterAllCallbacks(self)
+  if self.LBFGroup then
+    self.LBFGroup:Delete(true)
+  end
+  LSG:RemoveFrame(f)
+  f:SetParent(UIParent)
+  f:ClearAllPoints()
+  f:Hide()
+end
+
+--
+-- Events
+--
+
+function Bar:OnConfigModeChanged(event, mode)
+  self:SetConfigMode(mode)
+end
+
+--
+-- Accessors
+--
+
+function Bar:GetName()
+  return self.name
+end
+
+-- only ReAction:RenameBar() should call this function. Calling from any other
+-- context will desync the bar list in the ReAction class.
+function Bar:SetName(name)
+  if self.LBFGroup then
+    -- LBF doesn't offer a method of renaming a group, so delete and remake the group.
+    local c = self.config.ButtonFacade
+    local g = ReAction.LBF:Group(L["ReAction"], name)
+    for idx, b in self:IterateButtons() do
+      self.LBFGroup:RemoveButton(b:GetFrame(), true)
+      g:AddButton(b:GetFrame())
+    end
+    self.LBFGroup:Delete(true)
+    self.LBFGroup = g
+    self.LBFGroup:Skin(c.skinID, c.gloss, c.backdrop, c.colors)
+  end
+  self.name = name
+  if self.overlay then
+    self.overlay:SetLabel(self.name)
+  end
+end
+
+function Bar:GetFrame()
+  -- this method is included for documentation purposes. It is overridden
+  -- for each object in the :New() method.
+  error("Invalid Bar object: used without initialization")
+end
+
+function Bar:GetButton(idx)
+  return self.buttons[idx]
+end
+
+function Bar:GetButtonClass()
+  return self.buttonClass
+end
+
+function Bar:GetConfig()
+  return self.config
+end
+
+function Bar:GetAnchor()
+  local c = self.config
+  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:SetAnchor(point, frame, relativePoint, x, y)
+  local c = self.config
+  c.point = point or c.point
+  c.anchor = frame or c.anchor
+  c.relpoint = relativePoint or c.relpoint
+  c.x = x or c.x
+  c.y = y or c.y
+  self:ApplyAnchor()
+end
+
+function Bar:GetSize()
+  local f = self:GetFrame()
+  return f:GetWidth(), f:GetHeight()
+end
+
+function Bar:SetSize(w,h)
+  local f = self:GetFrame()
+  self.config.width = w
+  self.config.height = h
+  f:SetWidth(w)
+  f:SetHeight(h)
+end
+
+function Bar:GetButtonSize()
+  local w = self.config.btnWidth or 32
+  local h = self.config.btnHeight or 32
+  -- TODO: get from modules?
+  return w,h
+end
+
+function Bar:SetButtonSize(w,h)
+  if w > 0 and h > 0 then
+    self.config.btnWidth = w
+    self.config.btnHeight = h
+  end
+end
+
+function Bar:GetNumButtons()
+  local r,c = self:GetButtonGrid()
+  return r*c
+end
+
+function Bar:GetButtonGrid()
+  local cfg = self.config
+  local r = cfg.btnRows or 1
+  local c = cfg.btnColumns or 1
+  local s = cfg.spacing or 4
+  return r,c,s
+end
+
+function Bar:SetButtonGrid(r,c,s)
+  if r > 0 and c > 0 and s > 0 then
+    local cfg = self.config
+    cfg.btnRows = r
+    cfg.btnColumns = c
+    cfg.spacing = s
+  end
+  self.buttonClass:SetupBar(self)
+end
+
+function Bar:GetAlpha()
+  return self.config.alpha or 1.0
+end
+
+function Bar:SetAlpha(value)
+  self.config.alpha = value
+  self:GetFrame():SetAlpha(value or 1.0)
+  self:UpdateDefaultStateAlpha()
+end
+
+function Bar:IterateButtons()
+  -- iterator returns idx, button, but does NOT iterate in index order
+  return pairs(self.buttons)
+end
+
+--
+-- Methods
+--
+
+function Bar:SetConfigMode(mode)
+  self:SetSecureData("showAll",mode)
+  self:ShowControls(mode)
+  for idx, b in self:IterateButtons() do
+    b:ShowGridTemp(mode)
+    b:UpdateActionIDLabel(mode)
+  end
+end
+
+function Bar:SetKeybindMode(mode)
+  self:SetSecureData("showAll",mode)
+  for idx, b in self:IterateButtons() do
+    b:SetKeybindMode(mode)
+  end
+end
+
+function Bar:ApplyAnchor()
+  local f = self:GetFrame()
+  local c = self.config
+  local p = c.point
+
+  f:SetWidth(c.width)
+  f:SetHeight(c.height)
+  f:ClearAllPoints()
+  
+  if p then
+    local a = f:GetParent()
+    if c.anchor then
+      local bar = ReAction:GetBar(c.anchor)
+      if bar then
+        a = bar:GetFrame()
+      else
+        a = _G[c.anchor]
+      end
+    end
+    local fr = a or f:GetParent()
+    f:SetPoint(p, a or f:GetParent(), c.relpoint, c.x or 0, c.y or 0)
+  else
+    f:SetPoint("CENTER")
+  end
+
+  self:UpdateDefaultStateAnchor()
+end
+
+function Bar:ClipNButtons( n )
+  local cfg = self.config
+  local r = cfg.btnRows or 1
+  local c = cfg.btnColumns or 1
+
+  cfg.btnRows = ceil(n/c)
+  cfg.btnColumns = min(n,c)
+end
+
+function Bar:AddButton(idx, button)
+  local f = self:GetFrame()
+
+  self.buttons[idx] = button
+
+  -- Store a properly wrapped reference to the child frame as an attribute 
+  -- (accessible via "frameref-btn#")
+  f:SetFrameRef(format("btn%d",idx), button:GetFrame())
+
+  -- button constructors are responsible for calling SkinButton
+end
+
+function Bar:RemoveButton(button)
+  local idx = button:GetIndex()
+  if idx then
+    self:GetFrame():SetAttribute(format("frameref-btn%d",idx),nil)
+    self.buttons[idx] = nil
+  end
+  if self.LBFGroup then
+    self.LBFGroup:RemoveButton(button:GetFrame(),true)
+  end
+end
+
+function Bar:PlaceButton(button, baseW, baseH)
+  local idx = button:GetIndex()
+  if idx then 
+    local r, c, s = self:GetButtonGrid()
+    local bh, bw = self:GetButtonSize()
+    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 b = button:GetFrame()
+
+    b:ClearAllPoints()
+    b:SetPoint("TOPLEFT",x/scale,y/scale)
+    b:SetScale(scale)
+  end
+end
+
+function Bar:SkinButton( button, data )
+  if self.LBFGroup then
+    self.LBFGroup:AddButton(button:GetFrame(), data)
+  end
+end
+
+function Bar:UpdateShowGrid()
+  for idx, button in self:IterateButtons() do
+    button:UpdateShowGrid()
+  end
+end
+
+function Bar:ShowControls(show)
+  if show then
+    if not self.overlay then
+      self.overlay = Bar.Overlay:New(self) -- see Overlay.lua
+    end
+    self.overlay:Show()
+    self:RefreshSecureState()
+  elseif self.overlay then
+    self.overlay:Hide()
+  end
+end
+
+function Bar:RefreshControls()
+  if self.overlay and self.overlay:IsShown() then
+    self.overlay:RefreshControls()
+  end
+end
+
+function Bar:SetLabelSubtext(text)
+  if self.overlay then 
+    self.overlay:SetLabelSubtext(text) 
+  end
+end
+
+--
+-- Secure state functions
+--
+
+function Bar:GetSecureState()
+  local env = GetManagedEnvironment(self:GetFrame())
+  return env and env.state
+end
+
+function Bar:GetStateProperty(state, propname)
+  return tfetch(self:GetConfig(), "states", state, propname)
+end
+
+function Bar:SetStateProperty(state, propname, value)
+  local s = tbuild(self:GetConfig(), "states", state)
+  s[propname] = value
+  self:SetSecureStateData(state, propname, value)
+end
+
+function Bar:ApplyStates()
+  local states = tfetch(self:GetConfig(), "states")
+  if states then
+    self:SetStateDriver(states)
+  end
+end
+
+function Bar:CleanupStates()
+  self:SetStateDriver(nil)
+end
+
+function Bar:RefreshSecureState()
+  self:GetFrame():Execute(_reaction_refresh)
+end
+
+-- usage: SetSecureData(globalname, [tblkey1, tblkey2, ...], value)
+function Bar:SetSecureData( ... )
+  local n = select('#',...)
+  if n < 2 then
+    error("ReAction.Bar:SetSecureData() requires at least 2 arguments")
+  end
+  local f = self:GetFrame()
+  f:SetAttribute("data-depth",n-1)
+  f:SetAttribute("data-value",select(n,...))
+  for i = 1, n-1 do
+    local key = select(i,...)
+    if key == nil then
+      error("ReAction.Bar:SetSecureData() - nil table key in argument list (#"..i..")")
+    end
+    f:SetAttribute("data-key-"..i, key)
+  end
+  f:Execute(
+    [[
+      local n = self:GetAttribute("data-depth")
+      if n > 0 then
+        local value = self:GetAttribute("data-value")
+        local t = _G
+        for i = 1, n do
+          local key = self:GetAttribute("data-key-"..i)
+          if not key then return end
+          if not t[key] then
+            t[key] = newtable()
+          end
+          if i == n then
+            t[key] = value
+          else
+            t = t[key]
+          end
+        end
+      end
+    ]])
+  self:RefreshSecureState()
+end
+
+function Bar:SetSecureStateData( state, key, value )
+  self:SetSecureData("settings",state,key,value)
+end
+
+-- sets a snippet to be run as an extension to _onstate-reaction
+function Bar:SetSecureStateExtension( id, snippet )
+  if id == nil then
+    error("ReAction.Bar:SetSecureStateExtension() requires an id")
+  end
+  local f = self:GetFrame()
+  f:SetAttribute("input-secure-ext-id",id)
+  f:SetAttribute("secure-ext-"..id,snippet)
+  f:Execute(
+    [[
+      local id = self:GetAttribute("input-secure-ext-id")
+      if id then
+        extensions[id] = self:GetAttribute("secure-ext-"..id) or nil
+      end
+    ]])
+  self:RefreshSecureState()
+end
+
+function Bar:SetFrameRef( name, refFrame )
+  if refFrame then
+    local _, explicit = refFrame:IsProtected()
+    if not explicit then
+      refFrame = nil
+    end
+  end
+  if refFrame then
+    self:GetFrame():SetFrameRef(name,refFrame)
+  else
+    self:GetFrame():SetAttribute("frameref-"..name,nil)
+  end
+end
+
+function Bar:SetStateDriver( states )
+  if states then
+    for state, props in pairs(states) do
+      self:SetSecureStateData(state, "active_", true) -- make sure there's a 'settings' field for this state
+      for propname, value in pairs(props) do
+        if propname == "anchorFrame" then
+          self:SetFrameRef("anchor-"..state, _G[value])
+        elseif propname == "rule" then
+          -- do nothing
+        else
+          self:SetSecureStateData(state, propname, value)
+        end
+      end
+    end
+  end
+  local rule = states and self:BuildStateRule(states)
+  if rule then
+    RegisterStateDriver(self:GetFrame(),"reaction",rule)
+  elseif self.statedriver then
+    UnregisterStateDriver(self:GetFrame(),"reaction")
+  end
+  self.statedriver = rule
+  self:BuildStateKeybinds(states)
+  self:RefreshSecureState()
+end
+
+-- pass unit=nil to set up the unit elsewhere, if you want something more complex
+function Bar:RegisterUnitWatch( unit, enable )
+  local f = self:GetFrame()
+  if unit then
+    f:SetAttribute("unit",unit)
+  end
+  if enable then
+    if not self.unitwatch then
+      RegisterUnitWatch(self:GetFrame(),true)
+    end
+  elseif self.unitwatch then
+    UnregisterUnitWatch(self:GetFrame())
+  end
+  self.unitwatch = enable
+  self:RefreshSecureState()
+end
+
+function Bar:SetStateKeybind( key, state )
+  local f = self:GetFrame()
+  local binds = self.statebinds
+  if not binds then
+    binds = { }
+    self.statebinds = binds
+  end
+
+  -- clear the old binding, if any
+  if binds[state] then
+    SetOverrideBinding(f, false, binds[state], nil)
+  end
+
+  if key then
+    SetOverrideBindingClick(f, false, key, f:GetName(), state) -- state name is virtual mouse button
+  end
+  binds[state] = key
+end
+
+function Bar:GetStateKeybind( state )
+  if self.statebinds and state then
+    return self.statebinds[state]
+  end
+end
+
+function Bar:UpdateDefaultStateAnchor()
+  local point, frame, relPoint, x, y = self:GetAnchor()
+  local f = self:GetFrame()
+  f:SetAttribute("defaultAnchor-point",point)
+  f:SetAttribute("defaultAnchor-relPoint",relPoint)
+  f:SetAttribute("defaultAnchor-x",x)
+  f:SetAttribute("defaultAnchor-y",y)
+  self:SetFrameRef("defaultAnchor",_G[frame or "UIParent"])
+  f:Execute([[
+    for _, k in pairs(anchorKeys) do
+      defaultAnchor[k] = self:GetAttribute("defaultAnchor-"..k)
+    end
+    defaultAnchor.frame = self:GetAttribute("frameref-defaultAnchor")
+  ]])
+end
+
+function Bar:UpdateDefaultStateAlpha()
+  local f = self:GetFrame()
+  f:SetAttribute("defaultAlpha",self:GetAlpha())
+  f:Execute([[
+    defaultAlpha = self:GetAttribute("defaultAlpha")
+  ]])
+end
+
+---- secure state driver rules ----
+
+local playerClass = select(2, UnitClass("player"))
+local function ClassFilter(...)
+  for i = 1, select('#',...) do
+    if playerClass == select(i,...) then
+      return false
+    end
+  end
+  return true
+end
+
+local ruleformats = { 
+  stealth       = { format = "stealth",        filter = ClassFilter("ROGUE","DRUID") },
+  nostealth     = { format = "nostealth",      filter = ClassFilter("ROGUE","DRUID") },
+  shadowdance   = { format = "bonusbar:2",     filter = ClassFilter("ROGUE") },
+  shadowform    = { format = "form:1",         filter = ClassFilter("PRIEST") },
+  noshadowform  = { format = "noform",         filter = ClassFilter("PRIEST") },
+  battle        = { format = "stance:1",       filter = ClassFilter("WARRIOR") },
+  defensive     = { format = "stance:2",       filter = ClassFilter("WARRIOR") },
+  berserker     = { format = "stance:3",       filter = ClassFilter("WARRIOR") },
+  caster        = { format = "form:0/2/4/5/6", filter = ClassFilter("DRUID") },
+  bear          = { format = "form:1",         filter = ClassFilter("DRUID") },
+  cat           = { format = "form:3",         filter = ClassFilter("DRUID") },
+  tree          = { format = "form:5",         filter = ClassFilter("DRUID") },
+  moonkin       = { format = "form:5",         filter = ClassFilter("DRUID") },
+  demon         = { format = "form:2",         filter = ClassFilter("WARLOCK") },
+  nodemon       = { format = "noform",         filter = ClassFilter("WARLOCK") },
+  pet           = { format = "pet" },
+  nopet         = { format = "nopet" },
+  harm          = { format = "@target,harm" },
+  help          = { format = "@target,help" },
+  notarget      = { format = "@target,noexists" },
+  focusharm     = { format = "@focus,harm" },
+  focushelp     = { format = "@focus,help" },
+  nofocus       = { format = "@focus,noexists" },
+  raid          = { format = "group:raid" },
+  party         = { format = "group:party" },
+  solo          = { format = "nogroup" },
+  combat        = { format = "combat" },
+  nocombat      = { format = "nocombat" },
+  possess       = { format = "@vehicle,noexists,bonusbar:5" },
+  vehicle       = { format = "@vehicle,exists,bonusbar:5" },
+}
+
+function Bar.InitRuleFormats()
+  local forms = { }
+  for i = 1, GetNumShapeshiftForms() do
+    local _, name = GetShapeshiftFormInfo(i)
+    forms[name] = i;
+  end
+    -- use 9 if not found since 9 is never a valid stance/form
+  local defensive = forms[GetSpellInfo(71)] or 9
+  local berserker = forms[GetSpellInfo(2458)] or 9
+  local bear      = forms[GetSpellInfo(5487)] or 9
+  local aquatic   = forms[GetSpellInfo(1066)] or 9
+  local cat       = forms[GetSpellInfo(768)] or 9
+  local travel    = forms[GetSpellInfo(783)] or 9
+  local tree      = forms[GetSpellInfo(33891)] or 9
+  local moonkin   = forms[GetSpellInfo(24858)] or 9
+  local flight    = forms[GetSpellInfo(40120)] or forms[GetSpellInfo(33943)] or 9
+
+  ruleformats.defensive.format = "stance:"..defensive
+  ruleformats.berserker.format = "stance:"..berserker
+  ruleformats.caster.format    = format("form:0/%d/%d/%d", aquatic, travel, flight)
+  ruleformats.bear.format      = "form:"..bear
+  ruleformats.cat.format       = "form:"..cat
+  ruleformats.tree.format      = "form:"..tree
+  ruleformats.moonkin.format   = "form:"..moonkin
+end
+
+function Bar:BuildStateRule(states)
+  -- states is a table :
+  --   states[statename].rule = {
+  --     order = #,
+  --     type = "default"/"custom"/"any"/"all",
+  --     values = { ... }, -- keys of ruleformats[]
+  --     custom = "...",
+  --   }
+  local rules = { }
+  local default
+
+  for idx, state in ipairs(fieldsort(states, "rule", "order")) do
+    local c = states[state].rule
+    local type = c.type
+    if type == "default" then
+      default = default or state
+    elseif type == "custom" then
+      if c.custom then
+        -- strip out all spaces from the custom rule
+        table.insert(rules, format("%s %s", c.custom:gsub("%s",""), state))
+      end
+    elseif type == "any" or type == "all" then
+      if c.values then
+        local clauses = { }
+        for key, value in pairs(c.values) do
+          if ruleformats[key] and not ruleformats[key].filter then
+            table.insert(clauses, ruleformats[key].format)
+          end
+        end
+        if #clauses > 0 then
+          local sep = (type == "any") and "][" or ","
+          table.insert(rules, format("[%s] %s", table.concat(clauses,sep), state))
+        end
+      end
+    end
+  end
+  -- make sure that the default, if any, is last
+  if default then
+    table.insert(rules, default)
+  end
+  return table.concat(rules,";")
+end
+
+function Bar:BuildStateKeybinds( states )
+  if states then
+    for name, state in pairs(states) do
+      local rule = tfetch(state, "rule")
+      if rule and rule.type == "keybind" then
+        self:SetStateKeybind(rule.keybind, name)
+      else
+        self:SetStateKeybind(nil, name) -- this clears an existing keybind
+      end
+    end
+  end
+end
+