diff MultiCastButton.lua @ 257:920d17851a93 stable

Merge 1.1 beta 4 to stable
author Flick
date Tue, 12 Apr 2011 16:06:31 -0700
parents 65f2805957a0
children c918ff9ac787
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MultiCastButton.lua	Tue Apr 12 16:06:31 2011 -0700
@@ -0,0 +1,779 @@
+local addonName, addonTable = ...
+local ReAction = addonTable.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
+
+--[[
+  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.
+
+
+]]--
+
+
+--
+-- 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 )
+  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
+        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
+  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
+  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()
+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 )
+  Super.SetupBar(self,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
+
+  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(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: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
+
+  -- 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)
+end
+