changeset 161:d0a41fc7b0d7

Totem bar support
author Flick <flickerstreak@gmail.com>
date Fri, 21 Aug 2009 04:15:09 +0000
parents caec78119a17
children fc08372f0c7a
files classes/ActionButton.lua classes/MultiCastButton.lua classes/classes.xml locale/enUS.lua modules/Totem.lua modules/modules.xml
diffstat 6 files changed, 818 insertions(+), 6 deletions(-) [+]
line wrap: on
line diff
--- a/classes/ActionButton.lua	Sat Aug 08 00:04:04 2009 +0000
+++ b/classes/ActionButton.lua	Fri Aug 21 04:15:09 2009 +0000
@@ -372,8 +372,7 @@
 end
 
 function Action:UpdateIcon()
-  local action = self.actionID
-  local texture = GetActionTexture(action)
+  local texture, tLeft, tRight, tTop, tBottom = self:GetIconTexture()
   local icon = self.frames.icon
   local hotkey = self.frames.hotkey
   local f = self:GetFrame()
@@ -388,6 +387,9 @@
 
   if texture then
     icon:SetTexture(texture)
+    if tLeft then
+      icon:SetTexCoord(tLeft,tRight,tTop,tBottom)
+    end
     icon:Show()
     self.rangeTimer = -1
     f:SetNormalTexture("Interface\\Buttons\\UI-Quickslot2")
@@ -399,6 +401,10 @@
   end
 end
 
+function Action:GetIconTexture()
+  return GetActionTexture(self.actionID)
+end
+
 function Action:UpdateBorder()
   local action = self.actionID
   if ReAction:GetKeybindMode() then
@@ -456,8 +462,8 @@
 end
 
 function Action:UpdateUsable()
-  local isUsable, notEnoughMana = IsUsableAction(self.actionID)
-  local noRange = IsActionInRange(self.actionID) == 0
+  local isUsable, notEnoughMana = self:GetUsable()
+  local noRange = self:GetInRange()
 
   isUsable = self.vehicleExitMode or (isUsable and not noRange)
 
@@ -488,8 +494,20 @@
   end
 end
 
+function Action:GetUsable()
+  return IsUsableAction(self.actionID)
+end
+
+function Action:GetInRange()
+  return IsActionInRange(self.actionID) == 0
+end
+
 function Action:UpdateCooldown()
-  CooldownFrame_SetTimer(self.frames.cooldown, GetActionCooldown(self.actionID))
+  CooldownFrame_SetTimer(self.frames.cooldown, self:GetCooldown())
+end
+
+function Action:GetCooldown()
+  return GetActionCooldown(self.actionID)
 end
 
 function Action:UpdateFlash()
@@ -657,7 +675,9 @@
 end
 
 function Action:OnAttributeChanged( attr, value )
-  self:UpdateAction()
+  if attr ~= "statehidden" then
+    self:UpdateAction()
+  end
 end
 
 function Action:PostClick( )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/classes/MultiCastButton.lua	Fri Aug 21 04:15:09 2009 +0000
@@ -0,0 +1,639 @@
+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
+    - 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 1
+  multiCastSpellList   = newtable()
+  for i = 1, nMultiCastSlots do
+    tinsert(multiCastSpellList, newtable())
+  end
+]]
+
+local _onstate_multispellpage = -- function(self, stateid, newstate)
+[[
+  currentMultiCastPage = tonumber(newstate)
+  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 = { 
+  -- TODO
+  "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",
+  "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
+
+  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
+  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
+    for i, spell in ipairs(spells) do 
+      if spell and IsSpellKnown(spell) then
+        f:SetAttribute("spell-page"..i, spell)
+        if i == 1 then
+          -- TODO: store/restore last used summon
+          f:SetAttribute("spell",spell)
+        end
+      end
+    end
+  else
+    local baseAction = barFrame:GetAttribute("baseActionID") + TOTEM_PRIORITIES[idx-1]
+    f:SetAttribute("type","action")
+    f:SetAttribute("action", baseAction)
+    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
+
+  -- 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
+    return 0 -- no multicast capability
+  end
+
+  local slots = { }
+  
+  tinsert(slots, summon)
+
+  for i = 1, NUM_MULTI_CAST_BUTTONS_PER_PAGE do
+    local slotSpells = { 0, GetMultiCastTotemSpells(TOTEM_PRIORITIES[i]) }
+    maxIdx = max(maxIdx, #slotSpells)
+    tinsert(slots,slotSpells)
+  end
+
+  tinsert(slots, recall)
+
+  local barFrame = bar:GetFrame()
+
+  -- init bar secure environment
+  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)
+
+  for i, p in ipairs(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
+
+  return #slots
+end
+
--- a/classes/classes.xml	Sat Aug 08 00:04:04 2009 +0000
+++ b/classes/classes.xml	Fri Aug 21 04:15:09 2009 +0000
@@ -11,5 +11,6 @@
 <Script file="StanceButton.lua"/>
 <Script file="BagButton.lua"/>
 <Script file="VehicleExitButton.lua"/>
+<Script file="MultiCastButton.lua"/>
 
 </Ui>
\ No newline at end of file
--- a/locale/enUS.lua	Sat Aug 08 00:04:04 2009 +0000
+++ b/locale/enUS.lua	Fri Aug 21 04:15:09 2009 +0000
@@ -191,6 +191,10 @@
 "Hide Auras",
 "Do not show Paladin Auras as stances",
 
+-- Totem
+"Totem Bar",
+"Totem Buttons",
+
 -- Bag
 "Bag Bar",
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/Totem.lua	Fri Aug 21 04:15:09 2009 +0000
@@ -0,0 +1,147 @@
+--[[
+  ReAction Totem button module
+
+--]]
+
+-- local imports
+local ReAction = ReAction
+local L = ReAction.L
+local _G = _G
+
+-- button 
+local Button = ReAction.Button.MultiCast
+
+-- module declaration
+local moduleID = "Totem"
+local module = ReAction:NewModule( moduleID,
+  "AceEvent-3.0"
+  -- mixins go here
+)
+
+-- handlers
+function module:OnInitialize()
+  self.db = ReAction.db:RegisterNamespace( moduleID,
+    {
+      profile = { 
+        buttons = { }
+      }
+    }
+  )
+
+  self.buttons = { }
+
+  ReAction:RegisterOptions(self, self:GetOptions())
+
+  ReAction.RegisterCallback(self, "OnCreateBar", "OnRefreshBar")
+  ReAction.RegisterCallback(self, "OnDestroyBar")
+  ReAction.RegisterCallback(self, "OnRefreshBar")
+  ReAction.RegisterCallback(self, "OnEraseBar")
+  ReAction.RegisterCallback(self, "OnRenameBar")
+
+  -- TODO: register for learning new spells
+end
+
+function module:OnEnable()
+  ReAction:RegisterBarType(L["Totem Bar"], 
+    { 
+      type = moduleID ,
+      defaultButtonSize = 36,
+      defaultBarRows = 1,
+      defaultBarCols = 6,
+      defaultBarSpacing = 3
+    })
+
+end
+
+function module:OnDisable()
+  ReAction:UnregisterBarType(L["Totem Bar"])
+end
+
+function module:OnDestroyBar(event, bar, name)
+  local btns = self.buttons[bar]
+  if btns then
+    for _,b in pairs(btns) do
+      if b then
+        b:Destroy()
+      end
+    end
+    self.buttons[bar] = nil
+  end
+end
+
+function module:OnRefreshBar(event, bar, name)
+  if bar.config.type == moduleID then
+    local btns = self.buttons[bar]
+    if btns == nil then
+      btns = { }
+      self.buttons[bar] = btns
+    end
+    local profile = self.db.profile
+    if profile.buttons[name] == nil then
+      profile.buttons[name] = {}
+    end
+    local btnCfg = profile.buttons[name]
+
+    local r, c = bar:GetButtonGrid()
+    local n = r*c
+    n = min(n,Button.SetupBarHeader(bar))
+    for i = 1, n do
+      if btnCfg[i] == nil then
+        btnCfg[i] = {}
+      end
+      if btns[i] == nil then
+        local success, r = pcall(Button.New,Button,i,btnCfg,bar)
+        if success and r then
+          btns[i] = r
+          bar:AddButton(i,r)
+        else
+          geterrorhandler()(r)
+          n = i - 1
+          bar:ClipNButtons(n)
+          break
+        end
+      end
+      btns[i]:Refresh()
+    end
+    for i = n+1, #btns do
+      if btns[i] then
+        bar:RemoveButton(btns[i])
+        btns[i] = btns[i]:Destroy()
+        if btnCfg[i] then
+          btnCfg[i] = nil
+        end
+      end
+    end
+  end
+
+end
+
+function module:OnEraseBar(event, bar, name)
+  self.db.profile.buttons[name] = nil
+end
+
+function module:OnRenameBar(event, bar, oldName, newName)
+  local b = self.db.profile.buttons
+  b[newname], b[oldname] = b[oldname], nil
+end
+
+function module:RefreshAll()
+  for bar in pairs(self.buttons) do
+    self:OnRefreshBar(nil,bar,bar:GetName())
+  end
+end
+
+
+---- options ----
+function module:GetOptions()
+  return {
+    stance = 
+    {
+      name = L["Totem Buttons"],
+      type = "group",
+      args = {
+        -- TODO
+      }
+    }
+  }
+end
--- a/modules/modules.xml	Sat Aug 08 00:04:04 2009 +0000
+++ b/modules/modules.xml	Fri Aug 21 04:15:09 2009 +0000
@@ -11,5 +11,6 @@
 <Script file="Stance.lua"/>
 <Script file="Bag.lua"/>
 <Script file="VehicleExit.lua"/>
+<Script file="Totem.lua"/>
 
 </Ui>
\ No newline at end of file