view MultiCastButton.lua @ 293:7c596a5951ee

Added tag 1.1 beta 10 for changeset 276165a0e860
author Flick
date Fri, 05 Aug 2011 16:27:45 -0700
parents 276165a0e860
children
line wrap: on
line source
local _, ns = ...
local ReAction = ns.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 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

--@do-not-package@
--[[
  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: 
      - make whether to show the colored border configurable (looks really bad with ButtonFacade:Zoomed)
      - apply ButtonFacade to the flyout buttons? Or at least zoom the textures slightly?
      - use a multiplier with SetTexCoord on totem bar texture?

  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.


]]--
--@end-do-not-package@

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

  -- these are set up in bar:SetupBar()
  flyoutChildren = flyoutChildren or newtable()
  summonSpells = summonSpells or newtable()
]]

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 totemID = totemIDsBySlot[slot - (summonSlot or 0)]
  if totemID == 0 then
    totemID = "summon"
  end

  local lastButton, lastPage
  for page, b in ipairs(flyoutChildren) do
    b:Hide()
    b:SetAttribute("totemSlot",totemID)
    if slot == summonSlot then
      local spellID = self:GetParent():GetAttribute("spell-page"..page)
      if spellID then
        b:SetAttribute("type","changePage")
        b:SetAttribute("spell",spellID)
        b:Show()
        lastButton = b
        lastPage = page
      end
    else
      local spell = select(page, 0, GetMultiCastTotemSpells(totemID) )
      if spell then
        b:SetAttribute("type","multispell")
        b:SetAttribute("action", baseActionID + (currentPage - 1)*slotsPerPage + totemID)
        b:SetAttribute("spell", spell)
        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()
    control:CallMethod("UpdateFlyoutTextures",totemID)
  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. Shamelessly stolen from FrameXML/MultiCastActionBarFrame.lua
--
local TOTEM_TEXTURE = "Interface\\Buttons\\UI-TotemBar"
local FLYOUT_UP_BUTTON_HL_TCOORDS   = { 72/128,  92/128, 88/256,  98/256 }
local FLYOUT_DOWN_BUTTON_HL_TCOORDS = { 72/128,  92/128, 69/256,  79/256 }

local SLOT_EMPTY_TCOORDS = {
  [EARTH_TOTEM_SLOT] = {  66/128,  96/128,   3/256,  33/256 },
	[FIRE_TOTEM_SLOT]  = {  67/128,  97/128, 100/256, 130/256 },
	[WATER_TOTEM_SLOT] = {  39/128,  69/128, 209/256, 239/256 },
	[AIR_TOTEM_SLOT]   = {  66/128,  96/128,  36/256,  66/256 },
}

local SLOT_OVERLAY_TCOORDS = {
	[EARTH_TOTEM_SLOT] = {   1/128,  35/128, 172/256, 206/256 },
	[FIRE_TOTEM_SLOT]  = {  36/128,  70/128, 172/256, 206/256 },
	[WATER_TOTEM_SLOT] = {   1/128,  35/128, 207/256, 240/256 },
	[AIR_TOTEM_SLOT]   = {  36/128,  70/128, 137/256, 171/256 },
}

local FLYOUT_UP_BUTTON_TCOORDS = {
	["summon"]         = {  99/128, 127/128,  84/256, 102/256 },
	[EARTH_TOTEM_SLOT] = {  99/128, 127/128, 160/256, 178/256 },
	[FIRE_TOTEM_SLOT]  = {  99/128, 127/128, 122/256, 140/256 },
	[WATER_TOTEM_SLOT] = {  99/128, 127/128, 199/256, 217/256 },
	[AIR_TOTEM_SLOT]   = {  99/128, 127/128, 237/256, 255/256 },
}

local FLYOUT_DOWN_BUTTON_TCOORDS = {
	["summon"]         = {  99/128, 127/128,  65/256,  83/256 },
	[EARTH_TOTEM_SLOT] = {  99/128, 127/128, 141/256, 159/256 },
	[FIRE_TOTEM_SLOT]  = {  99/128, 127/128, 103/256, 121/256 },
	[WATER_TOTEM_SLOT] = {  99/128, 127/128, 180/256, 198/256 },
	[AIR_TOTEM_SLOT]   = {  99/128, 127/128, 218/256, 236/256 },
}

local FLYOUT_TOP_TCOORDS = {
	["summon"]         = {  33/128,  65/128,   1/256,  23/256 },
	[EARTH_TOTEM_SLOT] = {   0/128,  32/128,  46/256,  68/256 },
	[FIRE_TOTEM_SLOT]  = {  33/128,  65/128,  46/256,  68/256 },
	[WATER_TOTEM_SLOT] = {   0/128,  32/128,   1/256,  23/256 },
	[AIR_TOTEM_SLOT]   = {   0/128,  32/128,  91/256, 113/256 },
}

local FLYOUT_MIDDLE_TCOORDS = {
	["summon"]         = {  33/128,  65/128,  23/256,  43/256 },
	[EARTH_TOTEM_SLOT] = {   0/128,  32/128,  68/256,  88/256 },
	[FIRE_TOTEM_SLOT]  = {  33/128,  65/128,  68/256,  88/256 },
	[WATER_TOTEM_SLOT] = {   0/128,  32/128,  23/256,  43/256 },
	[AIR_TOTEM_SLOT]   = {   0/128,  32/128, 113/256, 133/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 buttonTypeID = "Totem"
local Super = ReAction.Button
local Action = ReAction.Button.Action
local MultiCast = setmetatable( 
  { 
    defaultBarConfig = { 
      type = buttonTypeID,
      btnWidth = 36,
      btnHeight = 36,
      btnRows = 1,
      btnColumns = 6,
      spacing = 3,
      buttons = { }
    },

    barType = L["Totem Bar"], 
    buttonTypeID = buttonTypeID
  },
  { __index = Action } )

ReAction.Button.MultiCast = MultiCast
ReAction:RegisterBarType(MultiCast)

function MultiCast:New( btnConfig, bar, idx )
  if idx < 1 or idx > NUM_MULTI_CAST_BUTTONS_PER_PAGE + 2 then
    ReAction:UserError(L["All %s buttons are in use for this bar, cannot create any more buttons"]:format(self.barType))
    error(nil)
  end

  self = Super.New(self, btnConfig, bar, idx, "SecureActionButtonTemplate, ActionButtonTemplate" )

  if not bar.hasMulticast or idx > bar.maxIndex then
    -- Not enough multicast capability to use this button
    self:Refresh()
    return self
  end

  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
        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
    self.totemSlot = 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
    if not f.overlayTex then
      local tx = f:CreateTexture("OVERLAY")
      tx:SetTexture(TOTEM_TEXTURE)
      tx:SetTexCoord(unpack(SLOT_OVERLAY_TCOORDS[self.totemSlot]))
      tx:SetWidth(34)
      tx:SetHeight(34)
      tx:SetPoint("CENTER")
      tx:Show()
      f.overlayTex = tx
    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
  for _, evt in pairs(eventList) do
    f:RegisterEvent(evt)
  end

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

  -- attach to skinner
  bar:SkinButton(self)

  f:Show()

  -- open arrow and flyout background textures
  if idx ~= bar.recallSlot then
    local arrow = f._arrowFrame or CreateFrame("Button", nil, f, "SecureFrameTemplate")
    f._arrowFrame = arrow
    arrow:SetWidth(28)
    arrow:SetHeight(18)
    arrow:SetPoint("BOTTOM",self:GetFrame(),"TOP",0,0) -- TODO: better anchoring
    arrow:SetNormalTexture(TOTEM_TEXTURE)
    local slot = self.totemSlot or "summon"
    arrow:GetNormalTexture():SetTexCoord( unpack(FLYOUT_UP_BUTTON_TCOORDS[slot]) )
    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.bar:GetFrame()
  local f = self:GetFrame()
  pcall( barFrame.UnwrapScript, barFrame, f, "OnEnter" ) -- ignore errors
  if f._arrowFrame then
    pcall( barFrame.UnwrapScript, barFrame, f._arrowFrame,"OnClick" ) -- ignore errors
  end
  Super.Destroy(self)
end

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

  local bar = self.bar
  if bar.hasMulticast == true and self.idx <= bar.maxIndex or ReAction:GetConfigMode() then
    self:GetFrame():Show()
  else
    self:GetFrame():Hide()
  end
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(SLOT_EMPTY_TCOORDS[self.totemSlot or 1])
  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)
    local slot = tonumber(frame:GetAttribute("totemSlot")) or 1
    frame.icon:SetTexCoord( unpack(SLOT_EMPTY_TCOORDS[slot]) )
  else
    frame.icon:SetTexture(GetSpellTexture(GetSpellInfo(spellID)))
    frame.icon:SetTexCoord(0,1,0,1)
  end
end

function MultiCast:SetupBar( bar )
  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

  local maxIndex = nTotemSlots
  if summonSlot then
    maxIndex = maxIndex + 1
  end
  if recallSlot then
    maxIndex = maxIndex + 1
  end

  bar.hasMulticast = nTotemSlots > 0
  bar.summonSlot   = summonSlot
  bar.recallSlot   = recallSlot
  bar.nTotemSlots  = nTotemSlots
  bar.maxIndex     = maxIndex

  if bar.hasMulticast == false then
    Super.SetupBar(self,bar)
    return -- no multicast capability
  end

  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(18)
    close:SetPoint("BOTTOM",flyout,"TOP")
    close:SetNormalTexture(TOTEM_TEXTURE)
    close:GetNormalTexture():SetTexCoord(unpack(FLYOUT_DOWN_BUTTON_TCOORDS["summon"]))
    close:SetHighlightTexture(TOTEM_TEXTURE)
    close:GetHighlightTexture():SetTexCoord( unpack(FLYOUT_DOWN_BUTTON_HL_TCOORDS) )
    f:SetFrameRef("close",close)
    f:WrapScript(close, "OnClick", _closeFlyout)
    close:Show()

    local midTx = flyout:CreateTexture("BACKGROUND")
    midTx:SetWidth(32)
    midTx:SetHeight(20)
    midTx:SetPoint("BOTTOM")
    midTx:SetTexture(TOTEM_TEXTURE)
    midTx:SetTexCoord(unpack(FLYOUT_MIDDLE_TCOORDS["summon"]))
    midTx:Show()

    local topTx = flyout:CreateTexture("BACKGROUND")
    topTx:SetWidth(32)
    topTx:SetHeight(20)
    topTx:SetTexture(TOTEM_TEXTURE)
    midTx:SetTexCoord(unpack(FLYOUT_TOP_TCOORDS["summon"]))
    topTx:SetPoint("BOTTOM",midTx,"TOP",0,-10)
    topTx:Show()

    function flyout:UpdateTextures(slot)
      slot = slot or "summon"
      close:GetNormalTexture():SetTexCoord(unpack(FLYOUT_DOWN_BUTTON_TCOORDS[slot]))
      midTx:ClearAllPoints()
      midTx:SetPoint("BOTTOM")
      midTx:SetPoint("TOP",close,"BOTTOM",0,0)
      midTx:SetTexCoord(unpack(FLYOUT_MIDDLE_TCOORDS[slot]))
      topTx:SetTexCoord(unpack(FLYOUT_TOP_TCOORDS[slot]))
    end

    -- 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:EnableMouse(true)
      b:RegisterForClicks(bar:GetConfig().clickDown and "AnyDown" or "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

  -- scale flyout frame
  local scale = bar:GetButtonSize() / 36
  flyout:SetScale(scale)

  function f:UpdateFlyoutTextures(slot)
    flyout:UpdateTextures(slot)
  end

  -- re-execute setup when new spells are loaded
  if not f.events_registered then
    f:RegisterEvent("UPDATE_MULTI_CAST_ACTIONBAR")
    f:RegisterEvent("PLAYER_ENTERING_WORLD")
      -- Bar.frame does not use OnEvent
    f:SetScript("OnEvent", 
      function()
        if not InCombatLockdown() then
          self:SetupBar(bar)
        end
      end)
    f.events_registered = true
  end

  f:Execute(_bar_init)

  Super.SetupBar(self,bar) -- create buttons after this is done
end