view classes/MultiCastButton.lua @ 167:eab7e7642dd6

Rewrote MultiCastButton to show prior to learning a summon ability, cleaned up implementation. NOTE: a typo fix will invalidate any existing keybindings to totem buttons.
author Flick <flickerstreak@gmail.com>
date Tue, 19 Oct 2010 16:49:40 +0000
parents 8241be11dcc0
children 07c76dbc0236
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 known totems that fit that slot. This function is available in
        the secure environment.

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

    - The default UI has Call of the Elements always selected on UI load. This module remembers the last
      selected one and restores it.


]]--


--
-- Secure snippets
--

-- bar
local _bar_init = -- function(self)
[[
  -- set up some globals in the secure environment
  flyout               = self:GetFrameRef("flyout")
  flyoutSlot           = nil
  summonSlot           = self:GetAttribute("summonSlot")
  recallSlot           = self:GetAttribute("recallSlot")
  baseActionID         = self:GetAttribute("baseActionID")
  slotsPerPage         = self:GetAttribute("slotsPerPage")
  currentPage          = currentPage or self:GetAttribute("lastSummon") or 1

  totemIDsBySlot = newtable()
  for i = 1, slotsPerPage do
    totemIDsBySlot[i] = self:GetAttribute("TOTEM_PRIORITY_"..i)
  end

  summonSpells = summonSpells or newtable() -- set up in bar:SetupBarHeader()
]]

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


-- buttons
local _childupdate = -- function(self, snippetid, message)
[[
  local t = self:GetAttribute("type")
  self:SetAttribute(t, self:GetAttribute(t.."-page"..currentPage))
]]

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 slot = self:GetAttribute("bar-idx")
  local arrow = owner:GetFrameRef("arrow-"..slot)
  if arrow and not arrow:IsShown() and not (flyout:IsVisible() and flyoutSlot == slot) then
    arrow:ClearAllPoints()
    arrow:SetPoint("BOTTOM",self,"TOP",0,0)
    arrow:Show()
    arrow:RegisterAutoHide(0)
    arrow:AddToAutoHide(self)
  end
]]


-- flyout arrow
local _arrow_openFlyout = -- function(self)
[[
  local slot = self:GetAttribute("bar-idx")
  local lastButton, lastPage
  for page, b in ipairs(flyoutChildren) do
    b:Hide()
    if slot == summonSlot then
      local spellID = self:GetParent():GetAttribute("spell-page"..page)
      print("got spell-page"..tostring(page).." = ".. tostring(spellID))
      if spellID then
        b:SetAttribute("type","changePage")
        b:SetAttribute("spell",spellID)
        b:Show()
        lastButton = b
        lastPage = page
      end
    else
      local offset = summonSlot or 0
      local totemID = totemIDsBySlot[slot - offset]
      local spells = newtable( 0, GetMultiCastTotemSpells(totemID) )
      if spells[page] then
        b:SetAttribute("type","multispell")
        b:SetAttribute("action", baseActionID + (currentPage - 1)*slotsPerPage + totemID)
        b:SetAttribute("spell", spells[page])
        b:Show()
        lastButton = b
        lastPage = page
      end
    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 lastPage then
    flyout:SetHeight(lastPage * 27 + (close and close:GetHeight() or 0))
  end
  flyout:Show()
  flyout:RegisterAutoHide(1) -- TODO: configurable
  flyout:AddToAutoHide(owner)
  flyoutSlot = slot
  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 )
  local maxIndex = bar.nTotemSlots or 0
  if bar.summonSlot then
    maxIndex = maxIndex + 1
  end
  if bar.recallSlot then
    maxIndex = maxIndex + 1
  end

  if not bar.hasMulticast or idx > maxIndex then
    return false
  end

  if idx < 1 then
    error("invalid index")
  end

  local name = format("ReAction_%s_Totem_%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 == bar.recallSlot) and 1 or bar:GetConfig().lastSummon or 1
  if idx == bar.recallSlot or idx == bar.summonSlot then
    f:SetAttribute("type","spell")
    local spells = (idx == bar.summonSlot) 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
        print("setting attribute spell-page"..i.." to "..spell)
        f:SetAttribute("spell-page"..i, spell)
      end
    end
  else
    local offset = bar.summonSlot and 1 or 0
    local slot = SHAMAN_TOTEM_PRIORITIES[idx - offset]
    local baseAction = barFrame:GetAttribute("baseActionID") + slot
    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)

  -- 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 ~= bar.recallSlot 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 ~= bar.recallSlot 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)
    barFrame:SetFrameRef("arrow-"..idx,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(frame)
  if GetCVar("UberTooltips") == "1" then
    GameTooltip_SetDefaultAnchor(GameTooltip, frame)
  else
    GameTooltip:SetOwner(frame)
  end
  local spell = frame:GetAttribute("spell")
  if spell == nil or spell == 0 then
    GameTooltip:SetText(MULTI_CAST_TOOLTIP_NO_TOTEM, HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
  else
    GameTooltip:SetSpellByID(spell,false,true)
  end
end

local function HideFlyoutTooltip()
  GameTooltip:Hide()
end

local function UpdateFlyoutIcon(frame)
  local spellID = frame:GetAttribute("spell")
  if spellID == 0 or spellID == nil then
    frame.icon:SetTexture(TOTEM_TEXTURE)
    frame.icon:SetTexCoord( unpack(EMPTY_SLOT_TCOORDS) )
  else
    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 slot = 0
  local nTotemSlots = 0
  local summonSlot = nil
  local recallSlot = nil

  -- figure out the capabilities of the character
	for i, spell in ipairs(TOTEM_MULTI_CAST_SUMMON_SPELLS) do 
    if spell and IsSpellKnown(spell) then
      slot = 1
      summonSlot = 1
    end
	end

  for i = 1, NUM_MULTI_CAST_BUTTONS_PER_PAGE do
		local totem = SHAMAN_TOTEM_PRIORITIES[i];
		if GetTotemInfo(totem) and GetMultiCastTotemSpells(totem) then
      nTotemSlots = nTotemSlots + 1
      slot = slot + 1
    end
  end

  slot = slot + 1
	for i, spell in ipairs(TOTEM_MULTI_CAST_RECALL_SPELLS) do 
    if spell and IsSpellKnown(spell) then
      recallSlot = slot
    end
	end

  if nTotemSlots == 0 then
    bar.hasMulticast = false -- no multicast capability
    return
  end

  bar.hasMulticast = true
  bar.summonSlot   = summonSlot
  bar.recallSlot   = recallSlot
  bar.nTotemSlots  = nTotemSlots


  local f = bar:GetFrame()

  -- init bar secure environment
  f:SetAttribute("lastSummon", bar:GetConfig().lastSummon)
  f:SetAttribute("summonSlot", summonSlot)
  f:SetAttribute("recallSlot", recallSlot)
  f:SetAttribute("slotsPerPage", NUM_MULTI_CAST_BUTTONS_PER_PAGE)
  f:SetAttribute("baseActionID", (NUM_ACTIONBAR_PAGES + GetMultiCastBarOffset() - 1)*NUM_ACTIONBAR_BUTTONS)
  for i, p in ipairs(SHAMAN_TOTEM_PRIORITIES) do
    f:SetAttribute("TOTEM_PRIORITY_"..i,p)
  end
  f:SetAttribute("_onstate-multispellpage", _onstate_multispellpage)

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

  -- create flyout container frame and close arrow
  local flyout = bar._flyoutFrame
  if not flyout then
    flyout = CreateFrame("Frame", nil, f, "SecureFrameTemplate")
    bar._flyoutFrame = flyout
    f:SetFrameRef("flyout",flyout)
    flyout.buttons = { }
    flyout:Hide()
    flyout:SetWidth(24)
    flyout:SetHeight(1)
    flyout:SetPoint("BOTTOM",f,"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) )
    f:SetFrameRef("close",close)
    f:WrapScript(close, "OnClick", _closeFlyout)

    -- create flyout buttons
    for i = 1, 10 do -- maximum 9 spells + 1 empty slot
      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)
      f:SetAttribute("flyout-child-idx",i)
      f:SetFrameRef("flyout-child",b)
      f:Execute([[
          flyoutChildren = flyoutChildren or newtable()
          flyoutChildren[self:GetAttribute("flyout-child-idx")] = self:GetFrameRef("flyout-child")
        ]])
      f:WrapScript(b, "OnClick", _flyout_child_preClick, _flyout_child_postClick)
      b:Show()
      flyout.buttons[i] = b
    end
  end

  f:Execute(_bar_init)
end