view modules/Action.lua @ 121:fb6c3a642ae3

Added proper vehicle bar support. The exit-vehicle button is a little kludgy, needs to be cleaned up later.
author Flick <flickerstreak@gmail.com>
date Mon, 09 Feb 2009 19:02:58 +0000
parents fb48811a8736
children 729232aeeb5e
line wrap: on
line source
--[[
  ReAction Action button module.

  The button module implements standard action button functionality by wrapping Blizzard's 
  ActionBarButtonTemplate frame and associated functions.

  It also provides action remapping support for multiple pages and possessed targets
  (Mind Control, Eyes of the Beast, Karazhan Chess event, various quests, etc).
--]]

-- local imports
local ReAction = ReAction
local L = ReAction.L
local _G = _G
local CreateFrame = CreateFrame
local format = string.format
local wipe = wipe

ReAction:UpdateRevision("$Revision$")

local weak = { __mode="k" }

-- libraries
local KB = LibStub("LibKeyBound-1.0")
local LBF -- initialized later

-- module declaration
local moduleID = "Action"
local module = ReAction:NewModule( moduleID )

-- Class declarations
local Button = { }
local Handle = { }
local PropHandler = { }

-- Event handlers
function module:OnInitialize()
  self.db = ReAction.db:RegisterNamespace( moduleID,
    { 
      profile = {
        bars = { },
      }
    }
  )
  self.handles = setmetatable({ }, weak)

  ReAction:RegisterBarOptionGenerator(self, "GetBarOptions")

  ReAction.RegisterCallback(self, "OnCreateBar")
  ReAction.RegisterCallback(self, "OnRefreshBar")
  ReAction.RegisterCallback(self, "OnDestroyBar")
  ReAction.RegisterCallback(self, "OnEraseBar")
  ReAction.RegisterCallback(self, "OnRenameBar")
  ReAction.RegisterCallback(self, "OnConfigModeChanged")

  LBF = LibStub("LibButtonFacade",true)

  KB.RegisterCallback(self, "LIBKEYBOUND_ENABLED")
  KB.RegisterCallback(self, "LIBKEYBOUND_DISABLED")
  KB.RegisterCallback(self, "LIBKEYBOUND_MODE_COLOR_CHANGED","LIBKEYBOUND_ENABLED")
end

function module:OnEnable()
  ReAction:RegisterBarType(L["Action Bar"], 
    { 
      type = moduleID,
      defaultButtonSize = 36,
      defaultBarRows = 1,
      defaultBarCols = 12,
      defaultBarSpacing = 3
    }, true)
  ReAction:GetModule("State"):RegisterStateProperty("page", nil, PropHandler.GetOptions(), PropHandler)
end

function module:OnDisable()
  ReAction:UnregisterBarType(L["Action Bar"])
  ReAction:GetModule("State"):UnregisterStateProperty("page")
end

function module:OnCreateBar(event, bar, name)
  if bar.config.type == moduleID then
    local profile = self.db.profile
    if profile.bars[name] == nil then
      profile.bars[name] = {
        buttons = { }
      }
    end
    if self.handles[bar] == nil then
      self.handles[bar] = Handle:New(bar, profile.bars[name])
    end
  end
end

function module:OnRefreshBar(event, bar, name)
  if self.handles[bar] then
    self.handles[bar]:Refresh()
  end
end

function module:OnDestroyBar(event, bar, name)
  if self.handles[bar] then
    self.handles[bar]:Destroy()
    self.handles[bar] = nil
  end
end

function module:OnEraseBar(event, bar, name)
  self.db.profile.bars[name] = nil
end

function module:OnRenameBar(event, bar, oldname, newname)
  b = self.db.profile.bars
  b[newname], b[oldname] = b[oldname], nil
end

function module:OnConfigModeChanged(event, mode)
  for _, h in pairs(self.handles) do
    h:SetConfigMode(mode)
  end
end

function module:LIBKEYBOUND_ENABLED(evt)
  for _, h in pairs(self.handles) do
    h:ShowGrid(true)
    h:SetKeybindMode(true)
  end
end

function module:LIBKEYBOUND_DISABLED(evt)
  for _, h in pairs(self.handles) do
    h:ShowGrid(false)
    h:SetKeybindMode(false)
  end
end


---- Interface ----
function module:GetBarOptions(bar)
  local h = self.handles[bar]
  if h then
    return h:GetOptions()
  end
end


---- Bar Handle ----

do
  local options = {
    hideEmpty = {
      name = L["Hide Empty Buttons"],
      order = 1,
      type = "toggle",
      width = "double",
      get  = "GetHideEmpty",
      set  = "SetHideEmpty",
    },
    lockButtons = {
      name = L["Lock Buttons"],
      desc = L["Prevents picking up/dragging actions.|nNOTE: This setting is overridden by the global setting in Blizzard's Action Buttons tab"],
      order = 2,
      type = "toggle",
      disabled = "LockButtonsDisabled",
      get = "GetLockButtons",
      set = "SetLockButtons",
    },
    lockOnlyCombat = {
      name = L["Only in Combat"],
      desc = L["Only lock the buttons when in combat"],
      order = 3,
      type = "toggle",
      disabled = "LockButtonsCombatDisabled",
      get = "GetLockButtonsCombat",
      set = "SetLockButtonsCombat",
    },
    pages = {
      name  = L["# Pages"],
      desc  = L["Use the Dynamic State tab to specify page transitions"],
      order = 4,
      type  = "range",
      min   = 1,
      max   = 10,
      step  = 1,
      get   = "GetNumPages",
      set   = "SetNumPages",
    },
    mindcontrol = {
      name = L["Mind Control Support"],
      desc = L["When possessing a target (e.g. via Mind Control), map the first 12 buttons of this bar to the possessed target's actions."],
      order = 5,
      type = "toggle",
      width = "double",
      set = "SetMindControl",
      get = "GetMindControl",
    },
    vehicle = {
      name = L["Vehicle Support"],
      desc = L["When on a vehicle, map the first 6 buttons of this bar to the vehicle actions. The vehicle-exit button is mapped to the 7th button. Pitch controls are not supported."],
      order = 6,
      type = "toggle",
      width = "double",
      get = "GetVehicle",
      set = "SetVehicle",
    },
    actions = {
      name   = L["Edit Action IDs"],
      order  = 7,
      type   = "group",
      inline = true,
      args   = {
        method = {
          name   = L["Assign"],
          order  = 1,
          type   = "select",
          width  = "double",
          values = { [0] = L["Choose Method..."],
                     [1] = L["Individually"],
                     [2] = L["All at Once"], },
          get    = "GetActionEditMethod",
          set    = "SetActionEditMethod",
        },
        rowSelect = {
          name   = L["Row"],
          desc   = L["Rows are numbered top to bottom"],
          order  = 2,
          type   = "select",
          width  = "half",
          hidden = "IsButtonSelectHidden",
          values = "GetRowList",
          get    = "GetSelectedRow",
          set    = "SetSelectedRow",
        },
        colSelect = {
          name   = L["Col"],
          desc   = L["Columns are numbered left to right"],
          order  = 3,
          type   = "select",
          width  = "half",
          hidden = "IsButtonSelectHidden",
          values = "GetColumnList",
          get    = "GetSelectedColumn",
          set    = "SetSelectedColumn",
        },
        pageSelect = {
          name   = L["Page"],
          order  = 4,
          type   = "select",
          width  = "half",
          hidden = "IsPageSelectHidden",
          values = "GetPageList",
          get    = "GetSelectedPage",
          set    = "SetSelectedPage",
        },
        single = {
          name   = L["Action ID"],
          usage  = L["Specify ID 1-120"],
          order  = 5,
          type   = "input",
          width  = "half",
          hidden = "IsButtonSelectHidden",
          get    = "GetActionID",
          set    = "SetActionID",
          validate = "ValidateActionID",
        },
        multi = {
          name   = L["ID List"],
          usage  = L["Specify a comma-separated list of IDs for each button in the bar (in order). Separate multiple pages with semicolons (;)"],
          order  = 6,
          type   = "input",
          multiline = true,
          width  = "double",
          hidden = "IsMultiIDHidden",
          get    = "GetMultiID",
          set    = "SetMultiID",
          validate = "ValidateMultiID",
        },
      },
    },
  }

  local meta = { __index = Handle }

  function Handle:New( bar, config )
    local self = setmetatable(
      {
        bar = bar,
        config = config,
        btns = { }
      }, 
      meta)
    
    if self.config.buttons == nil then
      self.config.buttons = { }
    end
    self:Refresh()
    self:SetKeybindMode(ReAction:GetKeybindMode())
    return self
  end

  function Handle:Refresh()
    local r, c = self.bar:GetButtonGrid()
    local n = r*c
    local btnCfg = self.config.buttons
    if n ~= #self.btns then
      for i = 1, n do
        if btnCfg[i] == nil then
          btnCfg[i] = {}
        end
        if self.btns[i] == nil then
          local b = Button:New(self, i, btnCfg[i], self.config)
          self.btns[i] = b
          self.bar:AddButton(i,b)
        end
      end
      for i = n+1, #self.btns do
        if self.btns[i] then
          self.bar:RemoveButton(self.btns[i])
          self.btns[i]:Destroy()
          self.btns[i] = nil
          btnCfg[i] = nil
        end
      end
    end
    local f = self.bar:GetFrame()
    for _, b in ipairs(self.btns) do
      b:Refresh()
    end
    f:SetAttribute("mindcontrol",self.config.mindcontrol)
    f:SetAttribute("vehicle",self.config.vehicle)
    f:Execute(
      [[
      doMindControl = self:GetAttribute("mindcontrol")
      doVehicle = self:GetAttribute("vehicle")
      control:ChildUpdate()
      ]])

    f:SetAttribute("_onstate-mc",
      -- function _onstate-mc(self, stateid, newstate)
      [[
        local oldMcVehicleState = mcVehicleState
        mcVehicleState = newstate
        control:ChildUpdate()
        if oldMcVehicleState == "vehicle" or mcVehicleState == "vehicle" then
          control:ChildUpdate("vehicle")
        end
      ]])
    RegisterStateDriver(f, "mc", "[target=vehicle,exists] vehicle; [bonusbar:5] mc; none")

    self:UpdateButtonLock()
  end

  function Handle:Destroy()
    for _,b in pairs(self.btns) do
      if b then
        b:Destroy()
      end
    end
  end

  function Handle:SetConfigMode(mode)
    for _, b in pairs(self.btns) do
      b:ShowGrid(mode)
      b:ShowActionIDLabel(mode)
    end
  end

  function Handle:ShowGrid(show)
    for _, b in pairs(self.btns) do
      b:ShowGrid(show)
    end
  end

  function Handle:UpdateButtonLock()
    local f = self.bar:GetFrame()
    f:SetAttribute("lockbuttons",self.config.lockButtons)
    f:SetAttribute("lockbuttonscombat",self.config.lockButtonsCombat)
    f:Execute(
      [[
        lockButtons = self:GetAttribute("lockbuttons")
        lockButtonsCombat = self:GetAttribute("lockbuttonscombat")
      ]])
  end

  function Handle:SetKeybindMode(mode)
    for _, b in pairs(self.btns) do
      b:SetKeybindMode(mode)
    end
  end

  function Handle:GetLastButton()
    return self.btns[#self.btns]
  end

    -- options handlers
  function Handle:GetOptions()
    return {
      type = "group",
      name = L["Action Buttons"],
      handler = self,
      args = options
    }
  end

  function Handle:SetHideEmpty(info, value)
    if value ~= self.config.hideEmpty then
      self.config.hideEmpty = value
      self:ShowGrid(not value)
    end
  end

  function Handle:GetHideEmpty()
    return self.config.hideEmpty
  end

  function Handle:GetLockButtons()
    return LOCK_ACTIONBAR == "1" or self.config.lockButtons
  end

  function Handle:SetLockButtons(info, value)
    self.config.lockButtons = value
    self:UpdateButtonLock()
  end

  function Handle:LockButtonsDisabled()
    return LOCK_ACTIONBAR == "1"
  end

  function Handle:GetLockButtonsCombat()
    return self.config.lockButtonsCombat
  end

  function Handle:SetLockButtonsCombat(info, value)
    self.config.lockButtonsCombat = value
    self:UpdateButtonLock()
  end

  function Handle:LockButtonsCombatDisabled()
    return LOCK_ACTIONBAR == "1" or not self.config.lockButtons
  end

  function Handle:GetNumPages()
    return self.config.nPages
  end

  function Handle:SetNumPages(info, value)
    self.config.nPages = value
    self:Refresh()
  end

  function Handle:GetMindControl()
    return self.config.mindcontrol
  end

  function Handle:SetMindControl(info, value)
    self.config.mindcontrol = value
    self:Refresh()
  end

  function Handle:GetVehicle()
    return self.config.vehicle
  end

  function Handle:SetVehicle(info, value)
    self.config.vehicle = value
    self:Refresh()
  end

  function Handle:GetActionEditMethod()
    return self.editMethod or 0
  end

  function Handle:SetActionEditMethod(info, value)
    self.editMethod = value
  end

  function Handle:IsButtonSelectHidden()
    return self.editMethod ~= 1
  end

  function Handle:GetRowList()
    local r,c = self.bar:GetButtonGrid()
    if self.rowList == nil or #self.rowList ~= r then
      local list = { }
      for i = 1, r do
        table.insert(list,i)
      end
      self.rowList = list
    end
    return self.rowList
  end

  function Handle:GetSelectedRow()
    local r, c = self.bar:GetButtonGrid()
    local row = self.selectedRow or 1
    if row > r then
      row = 1
    end
    self.selectedRow = row
    return row
  end

  function Handle:SetSelectedRow(info, value)
    self.selectedRow = value
  end

  function Handle:GetColumnList()
    local r,c = self.bar:GetButtonGrid()
    if self.columnList == nil or #self.columnList ~= c then
      local list = { }
      for i = 1, c do
        table.insert(list,i)
      end
      self.columnList = list
    end
    return self.columnList
  end

  function Handle:GetSelectedColumn()
    local r, c = self.bar:GetButtonGrid()
    local col = self.selectedColumn or 1
    if col > c then
      col = 1
    end
    self.selectedColumn = col
    return col
  end

  function Handle:SetSelectedColumn(info, value)
    self.selectedColumn = value
  end

  function Handle:IsPageSelectHidden()
    return self.editMethod ~= 1 or (self.config.nPages or 1) < 2
  end

  function Handle:GetPageList()
    local n = self.config.nPages or 1
    if self.pageList == nil or #self.pageList ~= n then
      local p = { }
      for i = 1, n do
        table.insert(p,i)
      end
      self.pageList = p
    end
    return self.pageList
  end

  function Handle:GetSelectedPage()
    local p = self.selectedPage or 1
    if p > (self.config.nPages or 1) then
      p = 1
    end
    self.selectedPage = p
    return p
  end

  function Handle:SetSelectedPage(info, value)
    self.selectedPage = value
  end

  function Handle:GetActionID()
    local row = self.selectedRow or 1
    local col = self.selectedColumn or 1
    local r, c = self.bar:GetButtonGrid()
    local n = (row-1) * c + col
    local btn = self.btns[n]
    if btn then
      return tostring(btn:GetActionID(self.selectedPage or 1))
    end
  end

  function Handle:SetActionID(info, value)
    local row = self.selectedRow or 1
    local col = self.selectedColumn or 1
    local r, c = self.bar:GetButtonGrid()
    local n = (row-1) * c + col
    local btn = self.btns[n]
    if btn then
      btn:SetActionID(tonumber(value), self.selectedPage or 1)
    end
  end

  function Handle:ValidateActionID(info, value)
    value = tonumber(value)
    if value == nil or value < 1 or value > 120 then
      return L["Specify ID 1-120"]
    end
    return true
  end

  function Handle:IsMultiIDHidden()
    return self.editMethod ~= 2
  end

  function Handle:GetMultiID()
    local p = { }
    for i = 1, self.config.nPages or 1 do
      local b = { }
      for _, btn in ipairs(self.btns) do
        table.insert(b, btn:GetActionID(i))
      end
      table.insert(p, table.concat(b,","))
    end
    return table.concat(p,";\n")
  end


  local function ParseMultiID(nBtns, nPages, s)
    if s:match("[^%d%s,;]") then
      ReAction:Print("items other than digits, spaces, commas, and semicolons in string",s)
      return nil
    end
    local p = { }
    for list in s:gmatch("[^;]+") do
      local pattern = ("^%s?$"):format(("%s*(%d+)%s*,"):rep(nBtns))
      local ids = { list:match(pattern) }
      if #ids ~= nBtns then
        ReAction:Print("found",#ids,"buttons instead of",nBtns)
        return nil
      end
      table.insert(p,ids)
    end
    if #p ~= nPages then
      ReAction:Print("found",#p,"pages instead of",nPages)
      return nil
    end
    return p
  end

  function Handle:SetMultiID(info, value)
    local p = ParseMultiID(#self.btns, self.config.nPages or 1, value)
    for page, b in ipairs(p) do
      for button, id in ipairs(b) do
        self.btns[button]:SetActionID(id, page)
      end
    end
  end

  function Handle:ValidateMultiID(info, value)
    local bad = L["Invalid action ID list string"]
    if value == nil or ParseMultiID(#self.btns, self.config.nPages or 1, value) == nil then
      return bad
    end
    return true
  end
end


------ State property options ------
do
  local pageOptions = {
    page = {
      name     = L["Show Page #"],
      order    = 11,
      type     = "select",
      width    = "half",
      disabled = "IsPageDisabled",
      hidden   = "IsPageHidden",
      values   = "GetPageValues",
      set      = "SetProp",
      get      = "GetPage",
    },
  }

  local function GetBarConfig(bar)
    return module.db.profile.bars[bar:GetName()]
  end

  function PropHandler.GetOptions()
    return pageOptions
  end

  function PropHandler:IsPageDisabled()
    local c = GetBarConfig(self.bar)
    local n = c and c.nPages or 1
    return not (n > 1)
  end

  function PropHandler:IsPageHidden()
    return not GetBarConfig(self.bar)
  end

  function PropHandler:GetPageValues()
    if not self._pagevalues then
      self._pagevalues = { }
    end
    local c = GetBarConfig(self.bar)
    if c then
      local n = c.nPages
        -- cache the results
      if self._npages ~= n then
        self._npages = n
        wipe(self._pagevalues)
        for i = 1, n do
          self._pagevalues["page"..i] = i
        end
      end
    end
    return self._pagevalues
  end

  function PropHandler:GetPage(info)
    return self:GetProp(info) or 1
  end

end

------ ActionID allocation ------
-- this needs to be high performance when requesting new IDs,
-- or certain controls will become sluggish. However, the new-request
-- infrastructure can be built lazily the first time that a new request
-- comes in (which will only happen at user config time: at static startup
-- config time all actionIDs should already have been assigned and stored
-- in the config file)

local IDAlloc
do
  local n = 120
  
  IDAlloc = setmetatable({ wrap = 1, freecount = n }, {__index = function() return 0 end})

  function IDAlloc:Acquire(id, hint)
    id = tonumber(id)
    hint = tonumber(hint)
    if id and (id < 1 or id > n) then
      id = nil
    end
    if hint and (hint < 1 or hint > n) then
      hint = nil
    end
    if id == nil then
      -- get a free ID
      if hint and self[hint] == 0 then
        -- use the hint if it's free
        id = hint
      elseif self.freecount > 0 then
        -- if neither the id nor the hint are defined or free, but
        -- the list is known to have free IDs, then start searching
        -- at the hint for a free one
        for i = hint or 1, n do
          if self[i] == 0 then
            id = i
            break
          end
        end
        -- self.wrap the search
        if id == nil and hint and hint > 1 then
          for i = 1, hint - 1 do
            if self[i] == 0 then
              id = i
              break
            end
          end
        end
      end
      if id == nil then
        -- if there are no free IDs, start wrapping at 1
        id = self.wrap
        self.wrap = id + 1
        if self.wrap > n then
          self.wrap = 1
        end
      end
    end
    if self[id] == 0 then
      self.freecount = self.freecount - 1
    end
    self[id] = self[id] + 1
    return id
  end

  function IDAlloc:Release(id)
    id = tonumber(id)
    if id and (id >= 1 or id <= n) then
      self[id] = self[id] - 1
      if self[id] == 0 then
        self.freecount = self.freecount + 1
        self.wrap = 1
      end
    end
  end
end

------ Button class ------
local frameRecycler = { }
local trash = CreateFrame("Frame")
local OnUpdate, GetActionName, GetHotkey
do
  local ATTACK_BUTTON_FLASH_TIME = ATTACK_BUTTON_FLASH_TIME
  local IsActionInRange = IsActionInRange

  function OnUpdate(frame, elapsed)
    -- note: This function taints frame.flashtime and frame.rangeTimer. Both of these
    --       are only read by ActionButton_OnUpdate (which this function replaces). In
    --       all other places they're just written, so it doesn't taint any secure code.
    if frame.flashing == 1 then
      frame.flashtime = frame.flashtime - elapsed
      if frame.flashtime <= 0 then
        local overtime = -frame.flashtime
        if overtime >= ATTACK_BUTTON_FLASH_TIME then
          overtime = 0
        end
        frame.flashtime = ATTACK_BUTTON_FLASH_TIME - overtime

        local flashTexture = frame.flash
        if flashTexture:IsShown() then
          flashTexture:Hide()
        else
          flashTexture:Show()
        end
      end
    end
    
    if frame.rangeTimer then
      frame.rangeTimer = frame.rangeTimer - elapsed;

      if frame.rangeTimer <= 0 then
        if IsActionInRange(frame.action) == 0 then
          frame.icon:SetVertexColor(1.0,0.1,0.1)
        else
          ActionButton_UpdateUsable(frame)
        end
        frame.rangeTimer = 0.1
      end
    end
  end

  function GetActionName(f)
    local b = f and f._reactionButton
    if b then
      return format("%s:%s", b.bar:GetName(), b.idx)
    end
  end

  function GetHotkey(f)
    return KB:ToShortKey(GetBindingKey(format("CLICK %s:LeftButton",f:GetName())))
  end

  -- This is a bit hokey : install a bare hook on ActionButton_UpdateHotkey because
  -- even though it's secure it's never called in a way that can cause taint. This is 
  -- for performance reasons to avoid having to hook frame:OnEvent securely.
  local UpdateHotkey_old = ActionButton_UpdateHotkeys
  ActionButton_UpdateHotkeys = function( frame, ... )
    local b = frame._reactionButton
    if b then
      b.hotkey:SetText( GetHotkey(frame) )
    else
      return UpdateHotkey_old(frame, ...)
    end
  end
end

local meta = {__index = Button}

function Button:New( handle, idx, config, barConfig )
  local bar = handle.bar

  -- create new self
  self = setmetatable( 
    { 
      bar = bar,
      idx = idx,
      config = config,
      barConfig = barConfig,
    }, meta )

  local name = config.name or ("ReAction_%s_%s_%d"):format(bar:GetName(),moduleID,idx)
  self.name = name
  config.name = name
  local lastButton = handle:GetLastButton()
  config.actionID = IDAlloc:Acquire(config.actionID, lastButton and lastButton.config.actionID) -- gets a free one if none configured
  self.nPages = 1
  
  -- have to recycle frames with the same name: CreateFrame() doesn't overwrite
  -- existing globals. Can't set to nil in the global because it's then tainted.
  local parent = bar:GetFrame()
  local f = frameRecycler[name]
  if f then
    f:SetParent(parent)
  else
    f = CreateFrame("CheckButton", name, parent, "ActionBarButtonTemplate")
    -- ditch the old hotkey text because it's tied in ActionButton_Update() to the
    -- standard binding.
    local hotkey = _G[name.."HotKey"]
    hotkey:SetParent(trash)
    hotkey = f:CreateFontString(nil, "ARTWORK", "NumberFontNormalSmallGray")
    hotkey:SetWidth(36)
    hotkey:SetHeight(18)
    hotkey:SetJustifyH("RIGHT")
    hotkey:SetJustifyV("TOP")
    hotkey:SetPoint("TOPLEFT",f,"TOPLEFT",-2,-2)
    f.hotkey = hotkey
    f.icon = _G[name.."Icon"]
    f.flash = _G[name.."Flash"]
    f:SetScript("OnUpdate",OnUpdate)
  end

  f._reactionButton = self

  self.hotkey = f.hotkey
  self.border = _G[name.."Border"]

  f:SetAttribute("action", config.actionID)
  f:SetAttribute("default-action", config.actionID)
  -- install mind control actions for all buttons just for simplicity
  if self.idx <= 12 then
    f:SetAttribute("mc-action", 120 + self.idx)
  end

  -- set a tooltip onEnter
  f:SetScript("OnEnter", 
    function(frame)
      if ReAction:GetKeybindMode() then
        KB:Set(frame)
      elseif frame.vehicleExitMode then
        GameTooltip_AddNewbieTip(frame, LEAVE_VEHICLE, 1.0, 1.0, 1.0, nil);
      else
        ActionButton_SetTooltip(frame)
      end
    end)
  
  -- set a _childupdate handler, called within the header's context
  f:SetAttribute("_childupdate", 
    -- function _childupdate(self, snippetid, message)
    [[
      local action = "default-action"
      if (doVehicle and mcVehicleState == "vehicle") or
         (doMindControl and mcVehicleState == "mc") then
        action = "mc-action"
      elseif page and state and page[state] then
        action = "action-"..page[state]
      end

      local value = self:GetAttribute(action)
      if value then
        self:SetAttribute("action",value)
      end
    ]])

  -- Install a handler for the 7th button (only) to show/hide a
  -- vehicle exit button. This is more than a little bit hack-ish and
  -- will be replaced in the next iteration with the reimplementation
  -- of action button functionality.
  if idx == 7 then
    local barFrame = bar:GetFrame()
    function barFrame:ShowVehicleExit(show)
      local tx = f.vehicleExitTexture
      if show then
        if not tx then
          tx = f:CreateTexture(nil,"ARTWORK")
          tx:SetAllPoints()
            -- copied from Blizzard/VehicleMenuBar.lua SkinsData
          tx:SetTexture("Interface\\Vehicles\\UI-Vehicles-Button-Exit-Up")
          tx:SetTexCoord(0.140625, 0.859375, 0.140625, 0.859375)
          f.vehicleExitTexture = tx
        end
        tx:Show()
        f.vehicleExitMode = true
      elseif tx then
        tx:SetTexCoord(0,1,0,1)
        tx:Hide()
        f.vehicleExitMode = false
      end
    end

    f:SetAttribute("macrotext","/run VehicleExit()")
    f:SetAttribute("_childupdate-vehicle",
      -- function _childupdate-vehicle(self, snippetid, message)
      [[
        local show = (mcVehicleState == "vehicle")
        if show then
          self:SetAttribute("type","macro")
          self:SetAttribute("showgrid",self:GetAttribute("showgrid")+1)
          self:Show()
        else
          self:SetAttribute("type","action")
          local showgrid = self:GetAttribute("showgrid")
          showgrid = showgrid - 1
          if showgrid < 0 then showgrid = 0 end
          self:SetAttribute("showgrid",self:GetAttribute("showgrid")-1)
          if showgrid <= 0 then
            self:Hide()
          end
        end
        control:CallMethod("ShowVehicleExit",show)
      ]])
  end

  -- install drag wrappers to lock buttons 
  bar:GetFrame():WrapScript(f, "OnDragStart",
    -- OnDragStart(self, button, kind, value, ...)
    [[
      if lockButtons and (PlayerInCombat() or not lockButtonsCombat) and not IsModifiedClick("PICKUPACTION") then
        return "clear"
      end
    ]])

  self.frame = f

  -- initialize the hide state
  f:SetAttribute("showgrid",0)
  self:ShowGrid(not barConfig.hideEmpty)
  if ReAction:GetConfigMode() then
    self:ShowGrid(true)
  end

  -- set the hotkey text
  self.hotkey:SetText( GetHotkey(self.frame) )

  -- show the ID label if applicable
  self:ShowActionIDLabel(ReAction:GetConfigMode())

  -- attach to skinner
  bar:SkinButton(self,
    {
      HotKey = self.hotkey,
    }
  )

  self:Refresh()
  return self
end

function Button:Destroy()
  local f = self.frame
  f:UnregisterAllEvents()
  f:Hide()
  f:SetParent(UIParent)
  f:ClearAllPoints()
  f:SetAttribute("_childupdate",nil)
  f:SetAttribute("_childupdate-vehicle",nil)
  if self.name then
    frameRecycler[self.name] = f
  end
  if self.config.actionID then
    IDAlloc:Release(self.config.actionID)
  end
  if self.config.pageactions then
    for _, id in ipairs(self.config.pageactions) do
      IDAlloc:Release(id)
    end
  end
  f._reactionButton = nil
  self.frame = nil
  self.config = nil
  self.bar = nil
end

function Button:Refresh()
  local f = self.frame
  self.bar:PlaceButton(self, 36, 36)
  self:RefreshPages()
end

function Button:GetFrame()
  return self.frame
end

function Button:GetName()
  return self.name
end

function Button:GetConfig()
  return self.config
end

function Button:GetActionID(page)
  if page == nil then
    -- get the effective ID
    return self.frame.action -- kept up-to-date by Blizzard's ActionButton_CalculateAction()
  else
    if page == 1 then
      return self.config.actionID
    else
      return self.config.pageactions and self.config.pageactions[page] or self.config.actionID
    end
  end
end

function Button:SetActionID( id, page )
  id = tonumber(id)
  page = tonumber(page)
  if id == nil or id < 1 or id > 120 then
    error("Button:SetActionID - invalid action ID")
  end
  if page and page ~= 1 then
    if not self.config.pageactions then
      self.config.pageactions = { }
    end
    if self.config.pageactions[page] then
      IDAlloc:Release(self.config.pageactions[page])
    end
    self.config.pageactions[page] = id
    IDAlloc:Acquire(self.config.pageactions[page])
    self.frame:SetAttribute(("action-page%d"):format(page),id)
  else
    IDAlloc:Release(self.config.actionID)
    self.config.actionID = id
    IDAlloc:Acquire(self.config.actionID)
    self.frame:SetAttribute("action",id)
    if self.config.pageactions then
      self.config.pageactions[1] = id
      self.frame:SetAttribute("action-page1",id)
    end
  end
end

function Button:RefreshPages( force )
  local nPages = self.barConfig.nPages
  if nPages and (nPages ~= self.nPages or force) then
    local f = self:GetFrame()
    local c = self.config.pageactions
    if nPages > 1 and not c then
      c = { }
      self.config.pageactions = c
    end
    for i = 1, nPages do
      if i > 1 then
        c[i] = IDAlloc:Acquire(c[i], self.config.actionID + (i-1)*self.bar:GetNumButtons())
      else
        c[i] = self.config.actionID  -- page 1 is the same as the base actionID
      end
      f:SetAttribute(("action-page%d"):format(i),c[i])
    end
    for i = nPages+1, #c do
      IDAlloc:Release(c[i])
      c[i] = nil
      f:SetAttribute(("action-page%d"):format(i),nil)
    end
    self.nPages = nPages
  end
end

function Button:ShowGrid( show )
  if not InCombatLockdown() then
    local f = self.frame
    local count = f:GetAttribute("showgrid")
    if show then
      count = count + 1
    else
      count = count - 1
    end
    if count < 0 then
      count = 0
    end
    f:SetAttribute("showgrid",count)

    if count >= 1 and not f:GetAttribute("statehidden") then
      if LBF then
        LBF:SetNormalVertexColor(self.frame, 1.0, 1.0, 1.0, 0.5)
      else
        self.frame:GetNormalTexture():SetVertexColor(1.0, 1.0, 1.0, 0.5);
      end
      f:Show()
    elseif count < 1 and not HasAction(self:GetActionID()) then
      f:Hide()
    end
  end
end

function Button:ShowActionIDLabel( show )
  local f = self:GetFrame()
  if show then
    local id = self:GetActionID()
    if not f.actionIDLabel then
      local label = f:CreateFontString(nil,"OVERLAY","GameFontNormalLarge")
      label:SetAllPoints()
      label:SetJustifyH("CENTER")
      label:SetShadowColor(0,0,0,1)
      label:SetShadowOffset(2,-2)
      f.actionIDLabel = label -- store the label with the frame for recycling

      f:HookScript("OnAttributeChanged", 
        function(frame, attr, value)
          if label:IsVisible() and attr:match("action") then
            label:SetText(tostring(frame.action))
          end
        end)
    end
    f.actionIDLabel:SetText(tostring(id))
    f.actionIDLabel:Show()
  elseif f.actionIDLabel then
    f.actionIDLabel:Hide()
  end
end

function Button:SetKeybindMode( mode )
  if mode then
    self.frame.GetActionName = GetActionName
    self.frame.GetHotkey     = GetHotkey
    -- set the border for all buttons to the keybind-enable color
    self.border:SetVertexColor(KB:GetColorKeyBoundMode())
    self.border:Show()
  elseif IsEquippedAction(self:GetActionID()) then
    self.border:SetVertexColor(0, 1.0, 0, 0.35) -- from ActionButton.lua
  else
    self.border:Hide()
  end
end