view classes/MultiCastButton.lua @ 166:8241be11dcc0

TOTEM_PRIORITIES -> SHAMAN_TOTEM_PRIORITIES TOC update 40000
author Flick <flickerstreak@gmail.com>
date Sat, 16 Oct 2010 21:53:57 +0000
parents ab5c37989986
children eab7e7642dd6
line wrap: on
line source
local ReAction = ReAction
local L = ReAction.L
local _G = _G
local CreateFrame = CreateFrame
local format = string.format
local unpack = unpack
local GetCVar = GetCVar
local GameTooltip_SetDefaultAnchor = GameTooltip_SetDefaultAnchor
local CooldownFrame_SetTimer = CooldownFrame_SetTimer
local InCombatLockdown = InCombatLockdown
local IsUsableSpell = IsUsableSpell
local IsUsableAction = IsUsableAction
local IsSpellKnown = IsSpellKnown
local IsSpellInRange = IsSpellInRange
local IsActionInRange = IsActionInRange
local GetSpellInfo = GetSpellInfo
local GetSpellCooldown = GetSpellCooldown
local GetActionCooldown = GetActionCooldown
local GetSpellTexture = GetSpellTexture
local GetActionTexture = GetActionTexture
local GetMultiCastTotemSpells = GetMultiCastTotemSpells

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


--[[
  Blizzard Constants:
    - NUM_MULTI_CAST_BUTTONS_PER_PAGE = 4
    - NUM_MULTI_CAST_PAGES = 3
    - SHAMAN_TOTEM_PRIORITIES = { } -- sets the order of the totems
    - TOTEM_MULTI_CAST_SUMMON_SPELLS = { } -- list of summon spellIDs
    - TOTEM_MULTI_CAST_RECALL_SPELLS = { } -- list of recall spellIDs

  Blizzard Events:
    - UPDATE_MULTI_CAST_ACTIONBAR

  Blizzard APIs:
    - GetMultiCastBarOffset() : returns 6

    - SetMultiCastSpell(actionID, spellID) (protected) OR
         SetAttribute("type","multispell")
         SetAttribute("action",actionID)
         SetAttribute("spell",spellID)

         note: multicast actionID page is NUM_ACTIONBAR_PAGES + GetMultiCastBarOffset(),
               so that's action ID 132-144.

    - spell1, spell2, spell3, ... = GetMultiCastTotemSpells(slot)
        returns spellIDs for all totems that fit that slot. 
        Note this is available in the secure environment, but because IsSpellKnown() is not,
        it makes it pretty much useless.

  Blizzard textures:
    All the textures for the multicast bar (arrows, empty-slot icons, etc) are part of a single
    texture: each texture uses SetTexCoord() to display only a slice of the textures. I suppose
    this is to slightly optimize texture load performance, but it makes the UI code more clumsy.

    Each totem button and arrow has a colored border indicating its elemental type.
      TODO: update code to use these pretty per-slot icons, or start setting a border.

  Design Notes:
    - Only the header has a secure context. All other frames execute in its context.

    - Each button is either type "spell" (summon/recall) or type "action" (totem action IDs are 
      GetBonusBarOffset()=6, 132-144) with 3 pages of 4 buttons. The paging is controlled by
      the summon flyout, which is also paged with the summon spells (the recall button is not paged)
    
    - A spell list is updated in the secure context at setup time (TODO: redo setup when learning new 
      spells) with the list of spells known for each slot.
    
    - Each button (except recall) has an arrow button which appears on mouseover and when clicked
      opens the flyout via a wrapped OnClick handler. When the flyout is open, the arrow does not
      appear.
      TODO: add an alt-button ("SHOWMULTICASTFLYOUT") statemachine to the bar to listen for alt key
      presses and open/close the bar. Tapping the alt key toggles the flyout.
    
    - A single flyout with N+1 (1 slot is to select no totem for the set) flyout-buttons is a child
      of the bar. Each time the flyout panel is opened, the individual  buttons grab their corresponding
      spell/type from the list, according to the slot which opened the flyout. Each button either sets
      the current page (summon) or sets a multispell to an actionID via type="multispell". None of them
      actually cast any spells (though, I suppose we could modify this so that e.g. configurable 
      right-click casts the spell). The flyout also has a close button which closes the flyout: the
      flyout-open code positions the close button anchored to the last button in the flyout (which 
      changes dynamically because each slot has a different number of items in the list).

    - Multicast sets are not stances, there's no need (or ability) to handle swapping sets if one of 
      the summon spells is cast from elsewhere. Additionally, in the default UI Call of the Elements 
      always appears as the active summon when the UI is loaded, which is bad design that could be improved
      upon. TODO: Store state in a config variable and restore it at load time.


]]--


--
-- Secure snippets
--

-- bar
local _bar_init = -- function(self)
[[
  -- set up some globals in the secure environment
  flyout               = self:GetFrameRef("flyout")
  flyoutChildren       = newtable()
  nMultiCastSlots      = self:GetAttribute("nMultiCastSlots")
  baseActionID         = self:GetAttribute("baseActionID")
  currentMultiCastPage = currentMultiCastPage or self:GetAttribute("lastSummon") or 1
  multiCastSpellList   = newtable()
  for i = 1, nMultiCastSlots do
    tinsert(multiCastSpellList, newtable())
  end
]]

local _onstate_multispellpage = -- function(self, stateid, newstate)
[[
  currentMultiCastPage = tonumber(newstate)
  control:CallMethod("UpdateLastSummon",currentMultiCastPage)
  control:ChildUpdate()
]]


-- buttons
local _childupdate = -- function(self, snippetid, message)
[[
  if self:GetAttribute("type") == "spell" then
    self:SetAttribute("spell", self:GetAttribute("spell-page"..currentMultiCastPage))
  elseif self:GetAttribute("type") == "action" then
    self:SetAttribute("action", self:GetAttribute("action-page"..currentMultiCastPage))
  end
]]

local _onEnter = -- function(self)
  -- for whatever reason, RegisterAutoHide is unreliable
  -- unless you re-anchor the frame prior to calling it.
  -- Even then, it's still not terribly reliable.
[[
  local idx = self:GetAttribute("bar-idx")
  if not (flyout:IsVisible() and flyoutIdx == idx) then
    local arrow = owner:GetFrameRef("arrow-"..idx)
    if arrow and not arrow:IsShown() then
      arrow:ClearAllPoints()
      arrow:SetPoint("BOTTOM",self,"TOP",0,0) -- TODO: better anchoring
      arrow:Show()
      arrow:RegisterAutoHide(0)
      arrow:AddToAutoHide(self)
    end
  end
]]

local _onLeave = -- function(self)
  -- to increase reliability (somewhat), re-register it for hide on leave
[[
  local arrow = owner:GetFrameRef("arrow-"..self:GetAttribute("bar-idx"))
  if arrow then
    arrow:RegisterAutoHide(0)
    arrow:AddToAutoHide(self)
  end
]]


-- flyout arrow
local _arrow_openFlyout = -- function(self)
[[
  local currentMultiCastSlot = self:GetAttribute("bar-idx")
  local lastButton, lastIdx
  for idx, b in ipairs(flyoutChildren) do
    b:Hide() -- force the OnShow handler to run later
    local spellID = multiCastSpellList[currentMultiCastSlot][idx]
    if spellID then
      b:SetAttribute("spell",spellID) -- does passing 0 work for no-totem? Do we have to convert to nil?
      if currentMultiCastSlot == 1 then
        b:SetAttribute("type","changePage")
      else
        b:SetAttribute("type","multispell")
        local totemID = owner:GetAttribute("TOTEM_PRIORITY_"..(currentMultiCastSlot - 1))
        b:SetAttribute("action", baseActionID + (currentMultiCastPage - 1)*(nMultiCastSlots-2) + totemID)
      end
      b:Show()
      lastButton = b
      lastIdx = idx
    end
  end

  local close = owner:GetFrameRef("close")
  if lastButton and close then
    close:ClearAllPoints()
    close:SetPoint("BOTTOM",lastButton,"TOP",0,0) -- TODO: better anchoring
    close:Show()
  end

  flyout:ClearAllPoints()
  flyout:SetPoint("BOTTOM",self,"BOTTOM",0,0)  -- TODO: better anchoring
  if lastIdx then
    flyout:SetHeight(lastIdx * 27 + (close and close:GetHeight() or 0))
  end
  flyout:Show()
  flyout:RegisterAutoHide(1) -- TODO: configurable
  flyout:AddToAutoHide(owner)
  flyoutIdx = currentMultiCastSlot
  self:Hide()
]]

local _closeFlyout = -- function(self)
[[
  flyout:Hide()
]]


-- flyout child buttons
local _flyout_child_preClick = -- function(self, button, down)
[[
  local button = button
  if self:GetAttribute("type") == "changePage" then
    owner:SetAttribute("state-multispellpage",self:GetAttribute("index"))
    self:GetParent():Hide()
    return false
  else
    return nil, "close"
  end
]]

local _flyout_child_postClick = -- function(self, message, button, down)
[[
  if message == "close" then
    self:GetParent():Hide() -- hide flyout after selecting
  end
]]


--
-- The Blizzard totem bar textures are all actually one big texture,
-- with texcoord offsets
--
local TOTEM_TEXTURE = "Interface\\Buttons\\UI-TotemBar"
local FLYOUT_UP_BUTTON_TCOORDS      = { 99/128, 127/128, 84/256, 102/256 }
local FLYOUT_UP_BUTTON_HL_TCOORDS   = { 72/128,  92/128, 88/256,  98/256 }
local FLYOUT_DOWN_BUTTON_TCOORDS    = { 99/128, 127/128, 65/256,  83/256 }
local FLYOUT_DOWN_BUTTON_HL_TCOORDS = { 72/128,  92/128, 69/256,  79/256 }
local EMPTY_SLOT_TCOORDS            = { 66/128,  96/128,  3/256,  33/256 }

local eventList = { 
  "ACTIONBAR_SLOT_CHANGED",
  "ACTIONBAR_UPDATE_STATE",
  "ACTIONBAR_UPDATE_USABLE",
  "ACTIONBAR_UPDATE_COOLDOWN",
  "UPDATE_BINDINGS",
  "UPDATE_MULTI_CAST_ACTIONBAR",
}

--
-- MultiCast Button class
-- Inherits implementation methods from Action button class, but circumvents the constructor
-- and redefines/removes some methods.
--
local Super = ReAction.Button
local Action = ReAction.Button.Action
local MultiCast = setmetatable( { }, { __index = Action } )
ReAction.Button.MultiCast = MultiCast

function MultiCast:New( idx, btnConfig, bar )
  if idx < 1 or idx > NUM_MULTI_CAST_BUTTONS_PER_PAGE + 2 then
    error("Multicast button index out of range")
  end

  if idx > bar.nMultiCastSlots then
    return false
  end

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

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

  -- attributes
  local page = (idx == NUM_MULTI_CAST_BUTTONS_PER_PAGE + 2) and 1 or (bar:GetConfig().lastSummon or 1)
  if idx == 1 or idx == NUM_MULTI_CAST_BUTTONS_PER_PAGE + 2 then
    f:SetAttribute("type","spell")
    local spells = idx == 1 and TOTEM_MULTI_CAST_SUMMON_SPELLS or TOTEM_MULTI_CAST_RECALL_SPELLS
    f:SetAttribute("spell",spells[page])
    for i, spell in ipairs(spells) do 
      if spell and IsSpellKnown(spell) then
        f:SetAttribute("spell-page"..i, spell)
      end
    end
  else
    local baseAction = barFrame:GetAttribute("baseActionID") + SHAMAN_TOTEM_PRIORITIES[idx-1]
    f:SetAttribute("type","action")
    f:SetAttribute("action", baseAction + (page - 1) * NUM_MULTI_CAST_BUTTONS_PER_PAGE)
    for i = 1, NUM_MULTI_CAST_PAGES do
      f:SetAttribute("action-page"..i, baseAction + (i-1) * NUM_MULTI_CAST_BUTTONS_PER_PAGE)
    end
  end
  f:SetAttribute("bar-idx",idx)
  barFrame:SetFrameRef("slot-"..idx,f)

  -- 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)

  -- secure handlers
  if idx ~= NUM_MULTI_CAST_BUTTONS_PER_PAGE + 2 then
    f:SetAttribute("_childupdate",_childupdate)
  end
  barFrame:WrapScript(f, "OnEnter", _onEnter)

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

  -- Set up a proxy for the icon texture for use with ButtonFacade
  self.frames.icon.SetTexCoordRaw = self.frames.icon.SetTexCoord
  self.frames.icon.SetTexCoord = function( tx, ... )
    if self:GetIconTexture() == TOTEM_TEXTURE then
      tx:SetTexCoordRaw(unpack(EMPTY_SLOT_TCOORDS))
    else
      tx:SetTexCoordRaw(...)
    end
  end

  -- attach to skinner
  bar:SkinButton(self)

  f:Show()

  -- open arrow
  if idx ~= NUM_MULTI_CAST_BUTTONS_PER_PAGE + 2 then
    local arrow = CreateFrame("Button", nil, f, "SecureFrameTemplate")
    arrow:SetWidth(28)
    arrow:SetHeight(12)
    arrow:SetPoint("BOTTOM",self:GetFrame(),"TOP",0,0) -- TODO: better anchoring
    arrow:SetNormalTexture(TOTEM_TEXTURE)
    arrow:GetNormalTexture():SetTexCoord( unpack(FLYOUT_UP_BUTTON_TCOORDS) )
    arrow:SetHighlightTexture(TOTEM_TEXTURE)
    arrow:GetHighlightTexture():SetTexCoord( unpack(FLYOUT_UP_BUTTON_HL_TCOORDS) )
    arrow:SetAttribute("bar-idx",idx)
    arrow:Hide()
    barFrame:WrapScript(arrow, "OnClick", _arrow_openFlyout)
    local arrowRef = "arrow-"..idx
    barFrame:SetFrameRef(arrowRef,arrow)
  end

  self:Refresh()

  return self
end

function MultiCast:Destroy()
  local barFrame = self:GetFrame()
  Super.Destroy(self)
end

function MultiCast:Refresh()
  Super.Refresh(self)
  self:UpdateAction()
end

function MultiCast:ShowGrid( show )
end

function MultiCast:ShowGridTemp( show )
end

function MultiCast:AcquireActionID()
end

function MultiCast:ReleaseActionID()
end

function MultiCast:UpdateShowGrid()
end

function MultiCast:UpdateBorder()
end

function MultiCast:UpdateMacroText()
end

function MultiCast:UpdateCount()
end

function MultiCast:UpdateCheckedState()
  local action = self:GetActionID()
  if action and IsCurrentAction(action) then
    self:GetFrame():SetChecked(1)
  else
    self:GetFrame():SetChecked(0)
  end
end

function MultiCast:RefreshHasActionAttributes()
end

function MultiCast:UpdateFlash()
end

function MultiCast:GetIconTexture()
  local tx
  if self.spellID then
    tx = GetSpellTexture(GetSpellInfo(self.spellID))
  elseif self.actionID then
    tx = GetActionTexture(self.actionID)
  end
  if tx then
    return tx
  else
    return TOTEM_TEXTURE, unpack(EMPTY_SLOT_TCOORDS)
  end
end

function MultiCast:UpdateAction()
  local action = self:GetActionID()
  if action then
    if action ~= self.actionID then
      self.actionID = action
      self:UpdateAll()
    end
  else
    local spellID = self:GetSpellID()
    if spellID ~= self.spellID then
      self.spellID = spellID
      self:UpdateAll()
    end
  end
end

function MultiCast:GetActionID(page)
  return self:GetFrame():GetAttribute("action")
end

function MultiCast:GetSpellID(page)
  return self:GetFrame():GetAttribute("spell")
end

function MultiCast:SetActionID( id  )
  error("Can not set action ID of multicast buttons")
end

function MultiCast:SetTooltip()
  local barFrame = self:GetFrame()
  if GetCVar("UberTooltips") == "1" then
    GameTooltip_SetDefaultAnchor(GameTooltip, barFrame)
  else
    GameTooltip:SetOwner(barFrame)
  end
  if self.spellID then
    GameTooltip:SetSpellByID(self.spellID,false,true)
  elseif self.actionID then
    GameTooltip:SetAction(self.actionID)
  end
end

function MultiCast:GetUsable()
  if self.spellID then
    return IsUsableSpell((GetSpellInfo(self.spellID)))
  elseif self.actionID then
    return IsUsableAction(self.actionID)
  end
end

function MultiCast:GetInRange()
  if self.spellID then
    return IsSpellInRange((GetSpellInfo(self.spellID))) == 0
  elseif self.actionID then
    return IsActionInRange(self.actionID) == 0
  end
end

function MultiCast:GetCooldown()
  if self.spellID then
    return GetSpellCooldown((GetSpellInfo(self.spellID)))
  elseif self.actionID then
    return GetActionCooldown(self.actionID)
  else
    return 0, 0, 0
  end
end

function MultiCast:UPDATE_MULTI_CAST_ACTIONBAR()
  self:UpdateAll()
end


--
-- flyout setup
--
local function ShowFlyoutTooltip(barFrame)
  if GetCVar("UberTooltips") == "1" then
    GameTooltip_SetDefaultAnchor(GameTooltip, barFrame)
  else
    GameTooltip:SetOwner(barFrame)
  end
  local spell = barFrame:GetAttribute("spell")
  if barFrame == 0 then
    GameTooltip:SetText(MULTI_CAST_TOOLTIP_NO_TOTEM, HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
  else
    GameTooltip:SetSpellByID(barFrame:GetAttribute("spell"),false,true)
  end
end

local function HideFlyoutTooltip()
  GameTooltip:Hide()
end

local function UpdateFlyoutIcon(frame)
  local spellID = frame:GetAttribute("spell")
  if spellID == 0 then
    frame.icon:SetTexture(TOTEM_TEXTURE)
    frame.icon:SetTexCoord( unpack(EMPTY_SLOT_TCOORDS) )
  elseif spellID then
    frame.icon:SetTexture(GetSpellTexture(GetSpellInfo(spellID)))
    frame.icon:SetTexCoord(0,1,0,1)
  end
end

function MultiCast.SetupBarHeader( bar ) -- call this as a static method
  local summon = { }
  local recall = { }
  local maxIdx = 1

	for idx, spell in ipairs(TOTEM_MULTI_CAST_SUMMON_SPELLS) do 
    if spell and IsSpellKnown(spell) then
      tinsert(summon,spell)
      maxIdx = max(idx,maxIdx)
    end
	end

	for idx, spell in ipairs(TOTEM_MULTI_CAST_RECALL_SPELLS) do 
    if spell and IsSpellKnown(spell) then
      tinsert(recall,spell)
      maxIdx = max(idx,maxIdx)
    end
	end

  if #summon == 0 and #recall == 0 then
    bar.nMultiCastSlots = 0 -- no multicast capability
    return
  end

  local slots = { }
  
  tinsert(slots, summon)

  for i = 1, NUM_MULTI_CAST_BUTTONS_PER_PAGE do
    local slotSpells = { 0, GetMultiCastTotemSpells(SHAMAN_TOTEM_PRIORITIES[i]) }
    maxIdx = max(maxIdx, #slotSpells)
    tinsert(slots,slotSpells)
  end

  tinsert(slots, recall)

  local barFrame = bar:GetFrame()

  -- init bar secure environment
  barFrame:SetAttribute("lastSummon",bar:GetConfig().lastSummon)
  barFrame:SetAttribute("nMultiCastSlots",#slots)
  barFrame:SetAttribute("baseActionID", (NUM_ACTIONBAR_PAGES + GetMultiCastBarOffset() - 1)*NUM_ACTIONBAR_BUTTONS)
  barFrame:SetAttribute("_onstate-multispellpage", _onstate_multispellpage)
  barFrame:Execute(_bar_init)

  function barFrame:UpdateLastSummon(value)
    bar:GetConfig().lastSummon = value
  end

  for i, p in ipairs(SHAMAN_TOTEM_PRIORITIES) do
    barFrame:SetAttribute("TOTEM_PRIORITY_"..i,p)
  end

  -- create flyout container frame and close arrow
  local flyout = bar._flyoutFrame
  if not flyout then
    flyout = CreateFrame("Frame", nil, barFrame, "SecureFrameTemplate")
    bar._flyoutFrame = flyout
    barFrame:SetFrameRef("flyout",flyout)
    flyout.buttons = { }
    flyout:Hide()
    flyout:SetWidth(24)
    flyout:SetHeight(1)
    flyout:SetPoint("BOTTOM",barFrame,"TOP",0,0)

    local close = CreateFrame("Button", nil, flyout, "SecureFrameTemplate")
    close:SetWidth(28)
    close:SetHeight(12)
    close:SetPoint("TOP")
    close:SetNormalTexture(TOTEM_TEXTURE)
    close:GetNormalTexture():SetTexCoord( unpack(FLYOUT_DOWN_BUTTON_TCOORDS) )
    close:SetHighlightTexture(TOTEM_TEXTURE)
    close:GetHighlightTexture():SetTexCoord( unpack(FLYOUT_DOWN_BUTTON_HL_TCOORDS) )
    barFrame:SetFrameRef("close",close)
    barFrame:WrapScript(close, "OnClick", _closeFlyout)
  end

  -- create flyout buttons
  for i = #flyout.buttons + 1, maxIdx do
    local b = CreateFrame("Button",nil,flyout,"SecureActionButtonTemplate")
    b:SetWidth(24)
    b:SetHeight(24)
    local prev = flyout.buttons[i-1]
    b:SetPoint("BOTTOM", prev or flyout, prev and "TOP" or "BOTTOM", 0, 3) -- TODO: better anchoring
    b.icon = b:CreateTexture("BACKGROUND")
    b.icon:SetAllPoints()
    b.icon:Show()
    b:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square")
    b:GetHighlightTexture():SetBlendMode("ADD")
    b:RegisterForClicks("AnyUp")
    b:SetScript("OnShow",UpdateFlyoutIcon)
    b:SetScript("OnEnter",ShowFlyoutTooltip)
    b:SetScript("OnLeave",HideFlyoutTooltip)
    b:SetAttribute("index",i)
    b:Show()
    barFrame:WrapScript(b, "OnClick", _flyout_child_preClick, _flyout_child_postClick)
    flyout.buttons[i] = b
  end

  for i, b in ipairs(flyout.buttons) do
    barFrame:SetFrameRef("flyout-child",b)
    barFrame:Execute([[
        tinsert(flyoutChildren,self:GetFrameRef("flyout-child"))
      ]])
  end

  -- transfer the table of spell IDs into the secure environment
  for i, spells in ipairs(slots) do
    barFrame:SetAttribute("spell-slot", i)
    for j, spell in ipairs(spells) do
      barFrame:SetAttribute("spell-index", j)
      barFrame:SetAttribute("spell-id", spell)
      barFrame:Execute([[
          multiCastSpellList[self:GetAttribute("spell-slot")][self:GetAttribute("spell-index")] = self:GetAttribute("spell-id")
        ]])
    end
  end

  bar.nMultiCastSlots = #slots
end