view Bar.lua @ 297:f7a5676c9517

5.04 updates: - refresh button checked state and cooldown in C-code - display count for actions with charges - update action button events - pet action buttons - filter UNIT_AURA events by 'pet' type
author Flick
date Wed, 14 Nov 2012 16:34:14 -0800
parents 0cb6a9944497
children 6fbb55e5c624
line wrap: on
line source
local _, ns = ...
local ReAction = ns.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 = ns.tfetch
local tbuild = ns.tbuild
local fieldsort = ns.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)

  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

  -- 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()
  if ReAction:GetConfigMode() then
    self:SetConfigMode(true)
  end
  ReAction.RegisterCallback(self, "OnConfigModeChanged")
  if ReAction:GetKeybindMode() then
    self:SetKeybindMode(true)
  end

  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
    for _, b in pairs(self.buttons) do
      b:Refresh()
    end
  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)
  local cfg = self.config
  r = r or cfg.btnRows
  c = c or cfg.btnColumns
  s = s or cfg.spacing
  if r > 0 and c > 0 and s > 0 then
    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
  self.buttonClass:SetupBar(self) -- force a full refresh
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(f,true)
    end
  elseif self.unitwatch then
    UnregisterUnitWatch(f)
  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 = "stance: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:1",         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 = "overridebar" },
  vehicle       = { format = "vehicleui" },
}

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