view classes/ActionButton.lua @ 124:0c5017f6062d

More fixes for new action button implementation
author Flick <flickerstreak@gmail.com>
date Tue, 03 Mar 2009 22:10:45 +0000
parents 943eed2c7def
children 729232aeeb5e
line wrap: on
line source
local ReAction = ReAction
local L = ReAction.L
local _G = _G
local CreateFrame = CreateFrame
local GetBindingKey = GetBindingKey
local format = string.format
local IsUsableAction = IsUsableAction
local IsEquippedAction = IsEquippedAction
local IsConsumableAction = IsConsumableAction
local IsStackableAction = IsStackableAction
local GetActionText = GetActionText
local GetCVar = GetCVar
local GameTooltip_SetDefaultAnchor = GameTooltip_SetDefaultAnchor
local IsCurrentAction = IsCurrentAction
local IsAutoRepeatAction = IsAutoRepeatAction
local IsUsableAction = IsUsableAction
local IsAttackAction = IsAttackAction
local CooldownFrame_SetTimer = CooldownFrame_SetTimer
local GetActionCooldown = GetActionCooldown
local GetActionTexture = GetActionTexture
local ATTACK_BUTTON_FLASH_TIME = ATTACK_BUTTON_FLASH_TIME
local TOOLTIP_UPDATE_TIME = TOOLTIP_UPDATE_TIME
local IsActionInRange = IsActionInRange
local InCombatLockdown = InCombatLockdown
local HasAction = HasAction

ReAction:UpdateRevision("$Revision: 154 $")

--
-- Secure snippets
-- These are run within the context of the bar's sandbox, as the
-- buttons themselves do not have their own sandbox.
--
local _onstate_mc = -- function(self, stateid, newstate)
[[
  local oldMcVehicleState = mcVehicleState
  mcVehicleState = newstate
  control:ChildUpdate()
  if oldMcVehicleState == "vehicle" or mcVehicleState == "vehicle" then
    control:ChildUpdate("vehicle")
  end
]]

local _childupdate = -- function(self, snippetid, message)
[[
  local action = nil
  if (doVehicle and mcVehicleState == "vehicle") or
     (doMindControl and mcVehicleState == "mc") then
    local idx = self:GetAttribute("bar-idx")
    if idx and idx <= 12 then
      action = 120 + idx
    else
      action = 0
    end
  elseif page and state and page[state] then
    action = self:GetAttribute("action-page"..page[state])
  else
    action = self:GetAttribute("default-action")
  end

  self:SetAttribute("action",action)
  local hasaction = (action > 120) or self:GetAttribute("hasaction-"..action)

  if (self:GetAttribute("showgrid") + self:GetAttribute("showgrid-temp") == 0) and not hasaction then
    self:Hide()
  else
    self:Show()
  end
]]

local _childupdate_vehicleExit =  -- function(self, snippetid, message)
[[
  local show = (mcVehicleState == "vehicle")
  if show and doVehicle then
    self:SetAttribute("type","macro")
    self:SetAttribute("macrotext","/run VehicleExit()")
    self:Show()
  else
    self:SetAttribute("type","action")
  end
  control:CallMethod("ShowVehicleExit",show)
]]

local _childupdate_showgrid = -- function(self, snippetid, message)
[[
  self:SetAttribute("showgrid-temp",message or 0)
  local count = (message or 0) + (self:GetAttribute("showgrid") or 0)
  if count == 0 then
    local action = self:GetAttribute("action")
    local hasaction = (action > 120) or self:GetAttribute("hasaction-"..action)
    if hasaction then
      self:Show()
    else
      self:Hide()
    end
  else
    self:Show()
  end
]]

local _onDragStart = -- function(self, button, kind, value, ...)
[[
  if lockButtons and (PlayerInCombat() or not lockButtonsCombat) and not IsModifiedClick("PICKUPACTION") then
    return kind, value, ...
  else
    -- don't make any assumptions about hiding on grid show here, as we don't know if the 
    -- drag gets cancelled later. 
    return "action", self:GetAttribute("action")
  end
]]

local _onReceiveDrag = -- function(self, button, kind, value, ...)
[[
  if lockButtons and (PlayerInCombat() or not lockButtonsCombat) and not IsModifiedClick("PICKUPACTION") then
    return kind, value, ...
  else
    if kind == "spell" or kind == "item" or kind == "macro" then
      -- assume it's a valid action
      self:SetAttribute("hasaction-"..self:GetAttribute("action"),true)
    end
    return "action", self:GetAttribute("action")
  end
]]

--
-- private
--
local eventList = {
  "PLAYER_REGEN_ENABLED",
  "PLAYER_ENTERING_WORLD",
  "ACTIONBAR_PAGE_CHANGED",
  "ACTIONBAR_SLOT_CHANGED",
  "UPDATE_BINDINGS",
  "ACTIONBAR_UPDATE_STATE",
  "ACTIONBAR_UPDATE_USABLE",
  "ACTIONBAR_UPDATE_COOLDOWN",
  "UPDATE_INVENTORY_ALERTS",
  "PLAYER_TARGET_CHANGED",
  "TRADE_SKILL_SHOW",
  "TRADE_SKILL_CLOSE",
  "PLAYER_ENTER_COMBAT",
  "PLAYER_LEAVE_COMBAT",
  "START_AUTOREPEAT_SPELL",
  "STOP_AUTOREPEAT_SPELL",
  "UNIT_ENTERED_VEHICLE",
  "UNIT_EXITED_VEHICLE",
  "COMPANION_UPDATE",
}

--
-- Action Button class
--
local Super = ReAction.Button
local Action = setmetatable( { }, { __index = Super } )
ReAction.Button.Action = Action

function Action:New( idx, config, bar, idHint )
  local name = format("ReAction_%s_Action_%d",bar:GetName(),idx)
 
  self = Super.New(self, name, config, bar, idx, "SecureActionButtonTemplate, ActionButtonTemplate" )

  local f = self:GetFrame()
  local barFrame = bar:GetFrame()

  local frames = { }
  self.frames = frames
  frames.icon          = _G[name.."Icon"]
  frames.flash         = _G[name.."Flash"]
  frames.hotkey        = _G[name.."HotKey"]
  frames.count         = _G[name.."Count"]
  frames.name          = _G[name.."Name"]
  frames.border        = _G[name.."Border"]
  frames.cooldown      = _G[name.."Cooldown"]
  frames.normalTexture = _G[name.."NormalTexture"]

  self.hotkey = frames.hotkey -- alias for Button methods
  self.border = frames.border -- alias for Button methods

  self.rangeTimer = TOOLTIP_UPDATE_TIME

  -- set up the base action ID
  self:SetActionIDPool("action",120)
  config.actionID = self:AcquireActionID(config.actionID, idHint)
  self.nPages = 1

  -- attribute setup
  f:SetAttribute("type","action")
  f:SetAttribute("checkselfcast", true)
  f:SetAttribute("checkfocuscast", true)
  f:SetAttribute("useparent-unit", true)
  f:SetAttribute("action", config.actionID)
  f:SetAttribute("default-action", config.actionID)
  f:SetAttribute("showgrid",0)
  f:SetAttribute("showgrid-temp",0)
  f:SetAttribute("bar-idx",idx)

  -- non secure scripts
  f:SetScript("OnEvent", function(frame, ...) self:OnEvent(...) end)
  f:SetScript("OnEnter", function(frame) self:OnEnter() end)
  f:SetScript("OnLeave", function(frame) self:OnLeave() end)
  f:SetScript("OnAttributeChanged", function(frame, attr, value) self:OnAttributeChanged(attr, value) end)
  f:SetScript("PostClick", function(frame, ...) self:PostClick(...) end)
  f:SetScript("OnUpdate", function(frame, elapsed) self:OnUpdate(elapsed) end)
  f:SetScript("OnDragStart", function(frame) self:OnDragStart() end)
  f:SetScript("OnReceiveDrag", function(frame) self:OnReceiveDrag() end)

  -- secure handlers
  f:SetAttribute("_childupate", _childupdate)
  f:SetAttribute("_childupdate-showgrid",_childupdate_showgrid)
  barFrame:WrapScript(f, "OnDragStart", _onDragStart)
  barFrame:WrapScript(f, "OnReceiveDrag", _onReceiveDrag)

  -- event registration
  f:EnableMouse(true)
  f:RegisterForDrag("LeftButton", "RightButton")
  f:RegisterForClicks("AnyUp")
  for _, evt in pairs(eventList) do
    f:RegisterEvent(evt)
  end

  -- attach to skinner
  bar:SkinButton(self)

  -- initial display
  self:ShowGrid(not bar:GetConfig().hideEmpty)
  if ReAction:GetConfigMode() then
    self:ShowGrid(true)
  end

  f:Show()

  self:Refresh()

  return self
end

function Action:Destroy()
  local f = self:GetFrame()

  f:UnregisterAllEvents()

  f:SetAttribute("_childupdate-vehicle",nil)

  self:ReleaseActionID(config.actionID)
  if self.config.pageactions then
    for _, id in ipairs(self.config.pageactions) do
      self:ReleaseActionID(id)
    end
  end
  
  Super:Destroy()
end

function Action:Refresh()
  self.bar:PlaceButton(self, 36, 36)
  self:RefreshPages()
  self:InstallVehicle()
  self:UpdateAction()
end

function Action:InstallVehicle()
  if self.idx == 7 and self.bar:GetConfig().vehicle then
    -- install vehicle-exit button on 7th button (only)
    f:SetAttribute("_childupdate-vehicle", _childupdate_vehicleExit)
    barFrame.ShowVehicleExit = function(bar,show)
      self:ShowVehicleExit(show)
    end
  else
    f:SetAttribute("_childupdate-vehicle",nil)
  end
end

function Action:UpdateAll()
  self:UpdateActionIDLabel(ReAction:GetConfigMode())
  self:UpdateHotkey()
  self:UpdateShowGrid()
  self:UpdateIcon()
  self:UpdateBorder()
  self:UpdateMacroText()
  self:UpdateCount()
  self:UpdateTooltip()
  self:UpdateCheckedState()
  self:UpdateUsable()
  self:UpdateCooldown()
  self:UpdateFlash()
end

function Action:UpdateAction()
  local action = self:GetActionID()
  if action ~= self.actionID then
    self.actionID = action
    self:UpdateAll()
  end
end

function Action:UpdateShowGrid()
  -- this is a little bit complicated because there's no
  -- secure driver to handle show/hide grid events.
  if InCombatLockdown() then
    self.showgridPending = true  -- handle after combat
  else
    self.showgridPending = false
    -- check if each action has an action or not, and flag an attribute
    -- so that the showgrid secure handler can make decisions accordingly
    local f = self:GetFrame()
    f:SetAttribute("hasaction-"..self.config.actionID, HasAction(self.config.actionID))
    if self.config.pageactions then
      for i = 1, self.nPages do
        f:SetAttribute("hasaction-"..self.config.pageactions[i], HasAction(self.config.pageactions[i]))
      end
    end
    -- the following is an out-of-combat show/hide to supplement the secure
    -- handling and clean up after it when it guesses
    if HasAction(self.actionID) then
      f:Show()
    else
      f:Hide()
    end
  end
end

function Action:UpdateIcon()
  local action = self.actionID
  local texture = GetActionTexture(action)
  local icon = self.frames.icon
  local hotkey = self.frames.hotkey
  local f = self:GetFrame()
  if texture then
    icon:SetTexture(texture)
    icon:Show()
    self.rangeTimer = -1
    f:SetNormalTexture("Interface\\Buttons\\UI-Quickslot2")
    hotkey:SetVertexColor(1.0, 1.0, 1.0)
  else
    icon:Hide()
    self.frames.cooldown:Hide()
    self.rangeTimer = nil
    f:SetNormalTexture("Interface\\Buttons\\UI-Quickslot")
    hotkey:SetVertexColor(0.6, 0.6, 0.6)
  end
end

function Action:UpdateBorder()
  local action = self.actionID
  if ReAction:GetKeybindMode() then
    self:UpdateKeybindModeDisplay(true)
  elseif IsEquippedAction(action) then
    self.frames.border:SetVertexColor(0, 1.0, 0, 0.35)
    self.frames.border:Show()
  else
    self.frames.border:Hide()
  end
end

function Action:UpdateMacroText()
  local action = self.actionID
  if not IsConsumableAction(action) and not IsStackableAction(action) then
    self.frames.name:SetText(GetActionText(action))
  else
    self.frames.name:SetText("")
  end
end

function Action:UpdateCount()
  local action = self.actionID
  if IsConsumableAction(action) or IsStackableAction(action) then
    self.frames.count:SetText(GetActionCount(action))
  else
    self.frames.count:SetText("")
  end
end

function Action:UpdateTooltip()
  local f = self:GetFrame()
  if GameTooltip:GetOwner() == f then
    self:SetTooltip()
  end
end

function Action:SetTooltip()
  local f = self:GetFrame()
  if GetCVar("UberTooltips") == "1" then
    GameTooltip_SetDefaultAnchor(GameTooltip, f)
  else
    GameTooltip:SetOwner(f)
  end
  GameTooltip:SetAction(self.actionID)
end

function Action:UpdateCheckedState()
  local action = self.actionID
  if IsCurrentAction(action) or IsAutoRepeatAction(action) then
    self:GetFrame():SetChecked(1)
  else
    self:GetFrame():SetChecked(0)
  end
end

function Action:UpdateUsable()
  local isUsable, notEnoughMana = IsUsableAction(self.actionID)
  if isUsable then
    self.frames.icon:SetVertexColor(1.0, 1.0, 1.0)
    self.frames.normalTexture:SetVertexColor(1.0, 1.0, 1.0)
  elseif notEnoughMana then
    self.frames.icon:SetVertexColor(0.5, 0.5, 1.0)
    self.frames.normalTexture:SetVertexColor(0.5, 0.5, 1.0)
  else
    self.frames.icon:SetVertexColor(0.4, 0.4, 0.4)
    self.frames.normalTexture:SetVertexColor(1.0, 1.0, 1.0)
  end
end

function Action:UpdateCooldown()
  CooldownFrame_SetTimer(self.frames.cooldown, GetActionCooldown(self.actionID))
end

function Action:UpdateFlash()
  local action = self.actionID
  self:SetFlash( (IsAttackAction(action) and IsCurrentAction(action)) or IsAutoRepeatAction(action) )
end

function Action:SetFlash(flash)
  if self.flashing ~= flash then
    self.flashing = flash
    self.flashtime = 0
    if not flash then
      self.frames.flash:Hide()
    end
    self:UpdateCheckedState()
  end
end

function Action:RunFlash(elapsed)
  if self.flashing then
    local flashtime = self.flashtime - elapsed
    self.flashtime = flashtime
    if flashtime <= 0 then
      local overtime = -flashtime
      if overtime >= ATTACK_BUTTON_FLASH_TIME then
        overtime = 0
      end
      flashtime = ATTACK_BUTTON_FLASH_TIME - overtime
      local flash = self.frames.flash
      if flash:IsShown() then
        flash:Hide()
      else
        flash:Show()
      end
    end
  end
end

function Action:RunRangeFinder(elapsed)
  local rangeTimer = self.rangeTimer
  if rangeTimer then
    rangeTimer = rangeTimer - elapsed
    self.rangeTimer = rangeTimer
    if rangeTimer <= 0 then
      if IsActionInRange(self.actionID) == 0 then
        self.frames.icon:SetVertexColor(1.0,0.1,0.1)
      else
        self:UpdateUsable()
      end
      self.rangeTimer = TOOLTIP_UPDATE_TIME
    end
  end
end

function Action:GetActionID(page)
  if page == nil then
    -- get the effective ID
    return self:GetFrame():GetAttribute("action")
  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 Action:SetActionID( id, page )
  id = tonumber(id)
  page = tonumber(page)
  if id == nil or id < 1 or id > 120 then
    error("Action:SetActionID - invalid action ID")
  end
  if page and page ~= 1 then
    if not self.config.pageactions then
      self.config.pageactions = { }
    end
    self:ReleaseActionID(self.config.pageactions[page])
    self.config.pageactions[page] = id
    self:AcquireActionID(self.config.pageactions[page])
    self.frame:SetAttribute("action-page"..page,id)
  else
    self:ReleaseActionID(self.config.actionID)
    self.config.actionID = id
    self:AcquireActionID(self.config.actionID)
    self.frame:SetAttribute("action",id)
    self.frame:SetAttribute("default-action",id)
    if self.config.pageactions then
      self.config.pageactions[1] = id
      self.frame:SetAttribute("action-page1",id)
    end
  end
end

function Action:RefreshPages( force )
  local nPages = self.bar:GetConfig().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] = self:AcquireActionID(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
      self:ReleaseActionID(c[i])
      c[i] = nil
      f:SetAttribute(("action-page%d"):format(i),nil)
    end
    self.nPages = nPages
  end
end

function Action.SetupBarHeader( bar ) -- call this as a static method
  local f = bar:GetFrame()
  local c = bar:GetConfig()
  f:SetAttribute("mindcontrol",c.mindcontrol)
  f:SetAttribute("vehicle",c.vehicle)
  f:Execute(
    [[
    doMindControl = self:GetAttribute("mindcontrol")
    doVehicle = self:GetAttribute("vehicle")
    control:ChildUpdate()
    ]])

  f:SetAttribute("_onstate-mc", _onstate_mc)
  RegisterStateDriver(f, "mc", "[target=vehicle,exists] vehicle; [bonusbar:5] mc; none")

  f:SetAttribute("lockbuttons",c.lockButtons)
  f:SetAttribute("lockbuttonscombat",c.lockButtonsCombat)
  f:Execute(
    [[
      lockButtons = self:GetAttribute("lockbuttons")
      lockButtonsCombat = self:GetAttribute("lockbuttonscombat")
    ]])
end


function Action.SetButtonLock( bar, lock, lockCombat ) -- call this as a static method
  local f = bar:GetFrame()
  f:SetAttribute("lockbuttons",lock)
  f:SetAttribute("lockbuttonscombat",lockCombat)
  f:Execute(
    [[
      lockButtons = self:GetAttribute("lockbuttons")
      lockButtonsCombat = self:GetAttribute("lockbuttonscombat")
    ]])
end


function Action:ShowVehicleExit(show)
  local f = self:GetFrame()
  local tx = f.vehicleExitTexture
  if show and self.bar:GetConfig().vehicle 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

function Action:OnEnter( )
  self:SetTooltip()
end

function Action:OnLeave( )
  GameTooltip:Hide()
end

function Action:OnAttributeChanged( attr, value )
  self:UpdateAction()
end

function Action:PostClick( )
  self:UpdateCheckedState()
end

function Action:OnUpdate( elapsed )
  self:RunFlash(elapsed)
  self:RunRangeFinder(elapsed)
end

function Action:OnDragStart()
  self:UpdateCheckedState()
  self:UpdateFlash()
end

function Action:OnReceiveDrag()
  self:UpdateCheckedState()
  self:UpdateFlash()
end

function Action:OnEvent(event, ...)
  if self[event] then
    self[event](self, event, ...)
  end
end

function Action:ACTIONBAR_SLOT_CHANGED(event, action)
  if action == 0 or action == self.actionID then
    self:UpdateAction()
  end
end

function Action:PLAYER_ENTERING_WORLD()
  self:UpdateAction()
end

function Action:ACTIONBAR_PAGE_CHANGED()
  self:UpdateAction()
end

function Action:UPDATE_BONUS_ACTIONBAR()
  self:UpdateAction()
end

function Action:UPDATE_BINDINGS()
  self:UpdateHotkey()
end

function Action:PLAYER_TARGET_CHANGED()
  self.rangeTimer = -1
end

function Action:ACTIONBAR_UPDATE_STATE()
  self:UpdateCheckedState()
end

function Action:TRADE_SKILL_SHOW()
  self:UpdateCheckedState()
end
Action.TRADE_SKILL_CLOSE = Action.TRADE_SKILL_CLOSE

function Action:UNIT_ENTERED_VEHICLE(event,unit)
  if unit == "player" then
    self:UpdateCheckedState()
  end
end
Action.UNIT_EXITED_VEHICLE = Action.UNIT_ENTERED_VEHICLE

function Action:COMPANION_UPDATE(event,unit)
  if unit == "mount" then
    self:UpdateCheckedState()
  end
end

function Action:ACTIONBAR_UPDATE_USABLE()
  self:UpdateUsable()
end

function Action:ACTIONBAR_UPDATE_COOLDOWN()
  self:UpdateCooldown()
end

function Action:PLAYER_ENTER_COMBAT()
  if IsAttackAction(self.actionID) then
    self:SetFlash(true)
  end
end

function Action:PLAYER_LEAVE_COMBAT()
  if IsAttackAction(self.actionID) then
    self:SetFlash(false)
  end
end

function Action:START_AUTOREPEAT_SPELL()
  if IsAutoRepeatAction(self.actionID) then
    self:SetFlash(true)
  end
end

function Action:STOP_AUTOREPEAT_SPELL()
  if not IsAttackAction(self.actionID) then
    self:SetFlash(false)
  end
end

function Action:PLAYER_REGEN_ENABLED()
  if self.showgridPending then
    self:UpdateShowGrid()
  end
end