diff classes/ReBar.lua @ 7:f920db5fc6b1

version 0.3
author Flick <flickerstreak@gmail.com>
date Tue, 20 Mar 2007 21:25:29 +0000
parents dfd829db3ad0
children c05fd3e18b4f
line wrap: on
line diff
--- a/classes/ReBar.lua	Tue Mar 20 21:20:20 2007 +0000
+++ b/classes/ReBar.lua	Tue Mar 20 21:25:29 2007 +0000
@@ -1,55 +1,155 @@
+-- private constants
+local anchoredLabelColor    = { r = 1.0, g = 0.82, b = 0.0 }
+local nonAnchoredLabelColor = { r = 1.0, g = 1.0,  b = 1.0 }
 
--- private constants
-local insideFrame  = 1
-local outsideFrame = 2
+local DRAG_UPDATE_RATE = 0.125  -- cap at 8 Hz
 
-local pointFindTable = {
-  BOTTOMLEFT  = function(f) return f:GetLeft(),  f:GetBottom() end,
-  BOTTOM      = function(f) return nil,          f:GetBottom() end,
-  BOTTOMRIGHT = function(f) return f:GetRight(), f:GetBottom() end,
-  RIGHT       = function(f) return f:GetRight(), nil end,
-  TOPRIGHT    = function(f) return f:GetRight(), f:GetTop() end,
-  TOP         = function(f) return nil,          f:GetTop() end,
-  TOPLEFT     = function(f) return f:GetLeft(),  f:GetTop() end,
-  LEFT        = function(f) return f:GetLeft(),  nil end,
+local nStancePages = {
+  WARRIOR = 3,
+  ROGUE   = 0,
+  HUNTER  = 0,
+  DRUID   = 3, -- updated to 4 if talented
+  PALADIN = 0,
+  SHAMAN  = 0, -- As far as I know, ghost wolf is not a proper shapeshift form, it's just a buff
+  MAGE    = 0,
+  PRIEST  = 0, -- updated to 1 if talented
+  WARLOCK = 0,
 }
 
-local oppositePointTable = {
-  BOTTOMLEFT  = "TOPRIGHT",
-  BOTTOM      = "TOP",
-  BOTTOMRIGHT = "TOPLEFT",
-  RIGHT       = "LEFT",
-  TOPRIGHT    = "BOTTOMLEFT",
-  TOP         = "BOTTOM",
-  TOPLEFT     = "BOTTOMRIGHT",
-  LEFT        = "RIGHT"
+local stanceMaps = {
+  WARRIOR = {
+    -- note: warriors are never in shapeshift form 0.
+    [1] = "*:1", -- battle stance. All states go to state (page) 1.
+    [2] = "*:2", -- defensive stance. All states go to state (page) 2.
+    [3] = "*:3", -- berserker stance. All states go to state (page) 3.
+  },
+  ROGUE   = {},
+  HUNTER  = {},
+  DRUID   = {
+    [0] = "*:1", -- humanoid form. All states go to state (page) 1.
+    [1] = "*:2", -- bear/dire bear form. All states go to state (page) 2.
+    [2] = "*:1", -- aquatic form. All states to go state (page) 1 (same as humanoid)
+    [3] = "*:3", -- cat form. All states go to state (page) 3
+    [4] = "*:1", -- travel form. All states go to state (page) 1 (same as humanoid)
+    [5] = "*:4", -- oomkin/tree form [talent only]. All states go to state (page) 4
+    [6] = "*:1", -- flight form. All states go to state (page) 1 (same as humanoid) (??? How does this work with oomkin/tree?)
+  },
+  PALADIN = {},
+  SHAMAN  = {},
+  MAGE    = {},
+  PRIEST  = {
+    [0] = "*:1", -- normal form. All states go to page 1.
+    [1] = "*:2", -- shadowform (talent only). All states go to page 2.
+  },
+  WARLOCK = {},
 }
 
-local anchoredLabelColor = { r =0.6, g = 0.2, b = 1.0 }
-local nonAnchoredLabelColor = { r = 1.0, g = 0.82, b = 0.0 }
-
--- private variables
-local stickyTargets = {
-  [UIParent] = insideFrame,
-  [WorldFrame] = insideFrame
+local stealthMaps = { 
+  WARRIOR = "",
+  ROGUE   = "1:2",  -- go to page 2 for rogues
+  HUNTER  = "",
+  DRUID   = "3:5",  -- we'll replace with 1:2 if stance mapping is not enabled, and page 5 with 6 if oomkin/tree
+  PALADIN = "",
+  SHAMAN  = "",
+  MAGE    = "",
+  PRIEST  = "",
 }
 
+local unstealthMaps = {
+  WARRIOR = "",
+  ROGUE   = "2:1",  -- go to page 1 for rogues
+  HUNTER  = "",
+  DRUID   = "5:3",  -- we'll replace with 2:1 if stance mapping is not enabled, and page 5 with 6 if oomkin/tree
+  PALADIN = "",
+  SHAMAN  = "",
+  MAGE    = "",
+  PRIEST  = "",
+}
+
+-- Immediately fix if a druid with oomkin or tree (note: can't have both)
+local _, playerClass = UnitClass("player")
+if playerClass == "DRUID" and GetNumShapeshiftForms() > 4 then
+  nStancePages.DRUID = 4
+  stanceMaps.DRUID[5] = "*:4"
+  stealthMaps.DRUID = "3:6"
+  unstealthMaps.DRUID ="6:3"
+end
+
+-- Immediately fix if a priest with shadowform
+if playerClass == "PRIEST" and GetNumShapeshiftForms() > 1 then
+  nStancePages.PRIEST = 2
+end
+
+local AceOO = AceLibrary("AceOO-2.0")
+
 -- ReBar is an Ace 2 class prototype object.
-ReBar = AceLibrary("AceOO-2.0").Class("AceEvent-2.0")
+ReBar = AceOO.Class("AceEvent-2.0", ReAnchor, ReAnchor.IAnchorable)
 
-local dewdrop = AceLibrary("Dewdrop-2.0")
 
+----------------------
+-- Static members
+----------------------
+-- IButtonClass is an interface required of any button class type (not the class objects themselves) to be used with the bar
+ReBar.IButtonClass = AceOO.Interface {
+  Acquire = "function",       -- btn = Acquire(config, barIdx, pages, buttonsPerPage)
+                              -- analogous to new(), this should re-use recycled frames for memory efficiency
+  Release = "function",       -- Release(btn) should hide and dispose of the button (and recycle it)
+}
+
+-- IButton is an interface required of any button objects to be used with the bar
+ReBar.IButton = AceOO.Interface {
+  GetActionFrame = "function",  -- obtains the action frame, presumably inherited from SecureActionButtonTemplate or similar.
+  BarLocked      = "function",  -- BarLocked() called when the bar is locked.
+  BarUnlocked    = "function",  -- BarUnlocked() called when the bar is unlocked.
+  PlaceButton    = "function",  -- PlaceButton(parent, anchorPoint, offsetX, offsetY, squareSz), one-stop position and size setting
+  SetPages       = "function",  -- SetPages(n): sets number of pages for multi-paging. Can error if paging not supported.
+}
+
+-- wrap UIParent and WorldFrame in objects which implement the ReAnchor.IAnchorable interface
+local UIParentPlaceable = {
+  GetFrame = function() return UIParent end,
+  GetAnchorage = function() return ReAnchor.anchorInside end,
+}
+
+local WorldFramePlaceable = {
+  GetFrame = function() return WorldFrame end,
+  GetAnchorage = function() return ReAnchor.anchorInside end,
+}
+
+ReBar.anchorTargets = {
+  UIParentPlaceable,
+  WorldFramePlaceable
+}
+
+ReBar.UIParentAnchorTarget = {
+  UIParentPlaceable
+}
+
+
+
+
+
+
+--------------------
+-- instance methods
+--------------------
+-- construction and destruction
 function ReBar.prototype:init( config, id )
   ReBar.super.prototype.init(self)
 
-  local buttonClass = config and config.btnConfig and config.btnConfig.type and getglobal(config.btnConfig.type)
   self.config  = config
   self.barID   = id
-  self.class   = { button = buttonClass }
   self.buttons = { }
+  self.locked  = true
+
+  self.buttonClass = config.btnConfig and config.btnConfig.type and getglobal(config.btnConfig.type)
+  if not AceOO.inherits(self.buttonClass, ReBar.IButton) then
+    error("ReBar: Supplied Button type does not meet required interface.")
+  end
+   
 
   -- create the bar and control widgets
-  self.barFrame     = CreateFrame("Frame", "ReBar_"..self.barID, UIParent, "ReBarTemplate")
+  self.barFrame     = CreateFrame("Frame", "ReBar"..id, config.parent and getglobal(config.parent) or UIParent, "ReBarTemplate")
   self.controlFrame = getglobal(self.barFrame:GetName().."Controls")
   self.controlFrame.reBar = self
   self.barFrame:SetClampedToScreen(true)
@@ -58,82 +158,113 @@
   self.labelString = getglobal(self.controlFrame:GetName().."LabelString")
   self.labelString:SetText(id)
 
+  -- initial stateheader state
+  self.barFrame.StateChanged = function() self:StateChanged() end
+  self.barFrame:SetAttribute("state",config.pages and config.pages.currentPage or 1) -- initial state
+
   -- initialize the bar layout
   self:ApplySize()
   self:ApplyAnchor()
+  self:AcquireButtons()
   self:LayoutButtons()
   self:ApplyVisibility()
 
-  -- add bar to stickyTargets list
-  stickyTargets[self.barFrame] = outsideFrame
-  
-  -- initialize dewdrop menu
-	dewdrop:Register(self.controlFrame, 'children', function()
-	    dewdrop:FeedAceOptionsTable(ReActionGlobalMenuOptions)
-	    dewdrop:FeedAceOptionsTable(GenerateReActionBarOptions(self))
-	    dewdrop:FeedAceOptionsTable(GenerateReActionButtonOptions(self))
-	  end,
-	  'cursorX', true, 
-	  'cursorY', true
-	)
+  if self.config.pages then
+    self:SetPages(self.config.pages.n)
+    self:ApplyAutoStanceSwitch()
+    self:ApplyAutoStealthSwitch()
+    self:RefreshPageControls()
+  end
+
+  -- register page up/down buttons with ReBound for keybinding
+  if ReBound then
+    ReBound:AddKeybindTarget(getglobal(self.barFrame:GetName().."PageUp"))
+    ReBound:AddKeybindTarget(getglobal(self.barFrame:GetName().."PageDown"))
+  end
+
+  -- add bar to anchorTargets list
+  table.insert(ReBar.anchorTargets, self)
 end
 
+function ReBar.prototype:Destroy()
+  local f = self.barFrame
 
-function ReBar.prototype:Destroy()
-  if self.barFrame == dewdrop:GetOpenedParent() then
-    dewdrop:Close()
-    dewdrop:Unregister(self.barFrame)
+  self:HideControls()
+  f:Hide()
+  f:ClearAllPoints()
+  f:SetParent(nil)
+  f:SetPoint("BOTTOMRIGHT", UIParent, "TOPLEFT", 0, 0)
+  
+  while #self.buttons > 0 do
+    self.buttonClass:Release(table.remove(self.buttons))
   end
 
-  self:HideControls()
-  self.barFrame:Hide()
-  self.barFrame:ClearAllPoints()
-  self.barFrame:SetParent(nil)
-  self.barFrame:SetPoint("BOTTOMRIGHT", UIParent, "TOPLEFT", 0, 0)
+  -- remove from anchorTargets table
+  for idx, b in ipairs(ReBar.anchorTargets) do
+    if b == self then
+      table.remove(ReBar.anchorTargets, idx)
+      break
+    end
+  end
   
-  -- need to keep around self.config for dewdrop menus in the process of deleting self 
+  -- remove from globals
+  local n = f:GetName()
+  setglobal(n, nil)
+  setglobal(n.."Control", nil)
+  setglobal(n.."Controls", nil)
+  setglobal(n.."ControlsLabelString", nil)
+  setglobal(n.."PageUp", nil)
+  setglobal(n.."PageDown", nil)
+  setglobal(n.."Page", nil)
+  setglobal(n.."PageNumber", nil)
+  setglobal(n.."PageNumberLabel", nil)
+  setglobal(n.."PageNumberLabelText", nil)
+end
 
-  while #self.buttons > 0 do
-    self.class.button:release(table.remove(self.buttons))
-  end
 
-  -- remove from sticky targets table
-  stickyTargets[self.barFrame] = nil
-  
-  -- remove from global table
-  -- for some reason after a destroy/recreate the globals still reference
-  -- the old frames
-  setglobal(self.barFrame:GetName(), nil)
-  setglobal(self.barFrame:GetName().."Controls", nil)
-  setglobal(self.controlFrame:GetName().."LabelString", nil)
+
+
+-- ReAnchor.IAnchorable interface implementation
+function ReBar.prototype:GetFrame()
+  return self.barFrame
 end
 
+function ReBar.prototype:GetAnchorage()
+  return ReAnchor.anchorOutside
+end
+
+
+
 
 -- show/hide the control frame
 function ReBar.prototype:ShowControls()
+  self.locked = false
   self.controlFrame:Show()
   for _, b in ipairs(self.buttons) do
     b:BarUnlocked()
+    b:DisplayVisibility()
   end
+  self:ApplyVisibility()
 end
 
 function ReBar.prototype:HideControls()
+  self.locked = true
   local b = self.barFrame
   if b.isMoving or b.resizing then
     b:StopMovingOrSizing()
     b:SetScript("OnUpdate",nil)
   end
-  -- close any dewdrop menu owned by us
-  if self.barFrame == dewdrop:GetOpenedParent() then
-    dewdrop:Close()
-  end
   for _, b in ipairs(self.buttons) do
     b:BarLocked()
+    b:DisplayVisibility()
   end
   self.controlFrame:Hide()
+  self:ApplyVisibility()
 end
 
-
+function ReBar.prototype:GetControlFrame()
+  return self.controlFrame
+end
 
 
 -- accessors
@@ -142,20 +273,265 @@
 end
 
 function ReBar.prototype:ToggleVisibility()
-  self.config.visible = not self.config.visible
+  self:SetVisibility( not self:GetVisibility() )
+end
+
+function ReBar.prototype:SetVisibility(v)
+  self.config.visible = v and true or false -- force data integrity
   self:ApplyVisibility()
 end
 
 function ReBar.prototype:GetOpacity()
-  return self.config.opacity or 100
+  return tonumber(self.config.opacity) or 100
 end
 
 function ReBar.prototype:SetOpacity( o )
-  self.config.opacity = tonumber(o)
-  self:ApplyVisibility()
-  return self.config.opacity
+  o = tonumber(o)
+  if o then
+    self.config.opacity = o
+    self:ApplyVisibility()
+    return self.config.opacity
+  end
 end
 
+function ReBar.prototype:GetButtonList()
+ return self.buttons
+end
+
+function ReBar.prototype:GetGrowLeft()
+  return self.config.growLeft
+end
+
+function ReBar.prototype:SetGrowLeft(g)
+  self.config.growLeft = g and true or false
+  self:LayoutButtons()
+end
+
+function ReBar.prototype:GetGrowUp()
+  return self.config.growUp
+end
+
+function ReBar.prototype:SetGrowUp(g)
+  self.config.growUp = g and true or false
+  self:LayoutButtons()
+end
+
+function ReBar.prototype:GetColumnMajor()
+  return self.config.columnMajor
+end
+
+function ReBar.prototype:SetColumnMajor(m)
+  self.config.columnMajor = m and true or false
+  self:LayoutButtons()
+end
+
+
+-- paging methods
+function ReBar.prototype:IsPagingDisabled()
+  return not (self.config.pages and self.config.pages.n > 1)
+end
+
+function ReBar.prototype:GetPages()
+  return self.config.pages and self.config.pages.n or 1
+end
+
+function ReBar.prototype:SetPages(n)
+  n = tonumber(n)
+  if n and n >= 1 then
+    self.config.pages = self.config.pages or { }
+    self.config.pages.n = n
+    for _, btn in pairs(self.buttons) do
+      btn:SetPages(n)
+    end
+    if n > 1 then
+      local statebutton = "0:page1;"
+      -- map states 1-n to 'page1'-'pagen'
+      -- page 0 is the same as page 1.
+      for i = 1, n do
+        statebutton = statebutton..i..":page"..i..";"
+      end
+      self.barFrame:SetAttribute("statebutton",statebutton)
+    else
+      self.barFrame:SetAttribute("statebutton",ATTRIBUTE_NOOP)
+    end
+    self.barFrame:SetAttribute("statemap-anchor-next","1-"..n)
+    self.barFrame:SetAttribute("statemap-anchor-prev",tostring(n).."-1")
+    self:RefreshPageControls()
+  end
+end
+
+function ReBar.prototype:GetAutoStanceSwitch()
+  return self.config.pages and self.config.pages.autoStanceSwitch
+end
+
+function ReBar.prototype:SetAutoStanceSwitch( s )
+  if not self.config.pages then
+    self.config.pages = { n = 1 }
+  end
+  self.config.pages.autoStanceSwitch = s and true or false
+  self:ApplyAutoStanceSwitch()
+end
+
+function ReBar.prototype:ToggleAutoStanceSwitch()
+  self:SetAutoStanceSwitch( not self:GetAutoStanceSwitch() )
+end
+
+function ReBar.prototype:ApplyAutoStanceSwitch()
+  local switch = self:GetAutoStanceSwitch()
+  local _, class = UnitClass("player")
+  if switch then
+    -- check that the number of pages available is sufficient
+    local totalPages = nStancePages[class] + (self:GetAutoStealthSwitch() and 1 or 0)
+    if self:GetPages() < totalPages then
+      self:SetPages(totalPages)
+    end
+    for form, spec in pairs(stanceMaps[class]) do
+      self.barFrame:SetAttribute("statemap-stance-"..form,spec)
+    end
+    -- set initial value
+    self.barFrame:SetAttribute("state-stance",GetShapeshiftForm(true))
+  else
+    for form, _ in pairs(stanceMaps[class]) do
+      self.barFrame:SetAttribute("statemap-stance-"..form, ATTRIBUTE_NOOP)
+    end
+  end
+end
+
+function ReBar.prototype:GetAutoStealthSwitch()
+  return self.config.pages and self.config.pages.autoStealthSwitch
+end
+
+function ReBar.prototype:SetAutoStealthSwitch( s )
+  if not self.config.pages then
+    self.config.pages = { n = 1 }
+  end
+  self.config.pages.autoStealthSwitch = s and true or false
+  self:ApplyAutoStealthSwitch()
+end
+
+function ReBar.prototype:ToggleAutoStealthSwitch()
+  self:SetAutoStealthSwitch( not self:GetAutoStealthSwitch() )
+end
+
+function ReBar.prototype:ApplyAutoStealthSwitch()
+  local switch = self:GetAutoStealthSwitch()
+  local _, class = UnitClass("player")
+  if switch then
+    -- check that the number of pages available is sufficient
+    local totalPages = (self:GetAutoStanceSwitch() and nStancePages[class] > 0 and nStancePages[class] or 1) + 1
+    if self:GetPages() < totalPages then
+      self:SetPages(totalPages)
+    end
+    local s, s2
+    if class == "DRUID" and not self:GetAutoStanceSwitch() then
+      -- change mapping for cat->prowl and prowl->cat to 1:2 and 2:1 since no stance mapping
+      s = "1:2"
+      s2 = "2:1"
+    end
+    self.barFrame:SetAttribute("statemap-stealth-1",s or stealthMaps[class])
+    self.barFrame:SetAttribute("statemap-stealth-0",s2 or unstealthMaps[class])
+    -- set initial value
+    self.barFrame:SetAttribute("state-stealth",IsStealthed() or 0)
+  else
+    self.barFrame:SetAttribute("statemap-stealth-1",ATTRIBUTE_NOOP)
+    self.barFrame:SetAttribute("statemap-stealth-0",ATTRIBUTE_NOOP)
+  end
+end
+
+function ReBar.prototype:ArePageControlsHidden()
+  return not ( self.config.pages and self.config.pages.showControls )
+end
+
+function ReBar.prototype:TogglePageControlsHidden()
+  if self.config.pages then
+    self.config.pages.showControls = not self.config.pages.showControls
+    self:RefreshPageControls()
+  end
+end
+
+function ReBar.prototype:GetPageControlsLoc()
+  return self.config.pages and self.config.pages.controlsLoc
+end
+
+function ReBar.prototype:SetPageControlsLoc(loc)
+  if self.config.pages then
+    self.config.pages.controlsLoc = loc
+    self:RefreshPageControls()
+  end
+end
+
+function ReBar.prototype:RefreshPageControls()
+  local b = self.barFrame;
+  local upArrow   = getglobal(b:GetName().."PageUp")
+  local downArrow = getglobal(b:GetName().."PageDown")
+  local pageNum   = getglobal(b:GetName().."PageNumber")
+
+  if self:GetPages() > 1 and self.config.pages.showControls then
+    local loc = self.config.pages.controlsLoc
+
+    pageNum:ClearAllPoints()
+    upArrow:ClearAllPoints()
+    downArrow:ClearAllPoints()
+
+    local vertical = { 0,0,0,1,1,0,1,1 }
+    local horizontal = { 0,1,1,1,0,0,1,0 }
+    local textures = { 
+      upArrow:GetNormalTexture(),
+      upArrow:GetPushedTexture(),
+      upArrow:GetDisabledTexture(),
+      upArrow:GetHighlightTexture(),
+      downArrow:GetNormalTexture(),
+      downArrow:GetPushedTexture(),
+      downArrow:GetDisabledTexture(),
+      downArrow:GetHighlightTexture(),
+    }
+
+    local offset = 10
+    local mult = (loc == "RIGHT" or loc == "TOP") and 1 or -1
+    local pageNumOffset = mult * (self.config.spacing/2 - offset)
+    local arrowOffset = mult * offset
+
+    if loc == "Blizzard" or loc == nil then
+      pageNum:SetPoint("LEFT", b, "RIGHT", 28, 0)
+      upArrow:SetPoint("LEFT", b, "RIGHT", -2, 9)
+      downArrow:SetPoint("LEFT", b, "RIGHT", -2, -10)
+      for _, tex in ipairs(textures) do
+        tex:SetTexCoord(unpack(vertical))
+      end
+    elseif loc == "RIGHT" or loc == "LEFT" then
+      local relPoint = loc == "RIGHT" and "LEFT" or "RIGHT"
+      pageNum:SetPoint(relPoint, b, loc, pageNumOffset, 0)
+      upArrow:SetPoint("BOTTOM",pageNum,"TOP",arrowOffset, -12)
+      downArrow:SetPoint("TOP",pageNum,"BOTTOM",arrowOffset, 12)
+      for _, tex in ipairs(textures) do
+        tex:SetTexCoord(unpack(vertical))
+      end
+    else
+      pageNum:SetPoint(loc == "BOTTOM" and "TOP" or "BOTTOM", b, loc, 0, pageNumOffset)
+      upArrow:SetPoint("LEFT",pageNum,"RIGHT",-12,arrowOffset)
+      downArrow:SetPoint("RIGHT",pageNum,"LEFT",12,arrowOffset)
+      for _, tex in ipairs(textures) do
+        tex:SetTexCoord(unpack(horizontal))
+      end
+    end
+    self:StateChanged()
+    upArrow:Show()
+    downArrow:Show()
+    pageNum:Show()
+  else
+    upArrow:Hide()
+    downArrow:Hide()
+    pageNum:Hide()
+  end
+end
+
+function ReBar.prototype:StateChanged()
+  local page = self.barFrame:GetAttribute("state") or 1
+  getglobal(self.barFrame:GetName().."PageNumberLabelText"):SetText(page)
+  if self.config.pages then self.config.pages.currentPage = page end
+end
+
+
 
 -- layout methods
 function ReBar.prototype:ApplySize()
@@ -163,14 +539,14 @@
   local spacing  = self.config.spacing or 4
   local rows     = self.config.rows or 1
   local columns  = self.config.columns or 12
-  local w = buttonSz * columns + spacing * (columns + 1)
-  local h = buttonSz * rows + spacing * (rows + 1)
+  local w = buttonSz * columns + spacing * columns
+  local h = buttonSz * rows + spacing * rows
   local f = self.barFrame
 
-  -- +1: avoid resizing oddities caused by fractional UI scale setting
-  f:SetMinResize(buttonSz + spacing*2 + 1, buttonSz + spacing*2 + 1)
-  f:SetWidth(w + 1)
-  f:SetHeight(h + 1)
+  -- + 0.1: avoid issues with UI scaling
+  f:SetMinResize(buttonSz + spacing + 0.1, buttonSz + spacing + 0.1)
+  f:SetWidth(w + 0.1)
+  f:SetHeight(h + 0.1)
 end
 
 function ReBar.prototype:ApplyAnchor()
@@ -178,9 +554,9 @@
   local f = self.barFrame
   if a then
     f:ClearAllPoints()
-    f:SetPoint(a.point,getglobal(a.to),a.relPoint,a.x,a.y)
+    f:SetPoint(a.point,getglobal(a.frame),a.relPoint,a.x,a.y)
     local color = anchoredLabelColor
-    if a.to == "UIParent" or a.to == "WorldFrame" then
+    if a.frame == "UIParent" or a.frame == "WorldFrame" then
       color = nonAnchoredLabelColor
     end
     self.labelString:SetTextColor(color.r, color.g, color.b)
@@ -188,18 +564,9 @@
 end
 
 function ReBar.prototype:ApplyVisibility()
-  local v = self.config.visibility
-  if type(v) == "table" then
-    if v.class then
-      local _, c = UnitClass("player")
-      v = v.class[c]
-    end
-  elseif type(v) == "string" then
-    local value = getglobal(v)
-    v = value
-  end
+  local v = self.config.visible or not self.locked
   
-  if self.config.opacity then
+  if tonumber(self.config.opacity) then
     self.barFrame:SetAlpha(self.config.opacity / 100)
   end
 
@@ -213,26 +580,55 @@
 function ReBar.prototype:LayoutButtons()
   local r = self.config.rows
   local c = self.config.columns
-  local n = r * c
   local sp = self.config.spacing
   local sz = self.config.size
   local gSize = sp + sz
+  local point = (self.config.growUp and "BOTTOM" or "TOP")..(self.config.growLeft and "RIGHT" or "LEFT")
+  local major = self.config.columnMajor and r or c
+
+  for i, b in ipairs(self.buttons) do
+    local x = sp/2 + gSize * math.fmod(i-1,major)
+    local y = sp/2 + gSize * math.floor((i-1)/major)
+    if self.config.columnMajor then  x,y = y,x end
+    if self.config.growLeft then x = -x end
+    if not self.config.growUp then y = -y end
+    b:PlaceButton(self.barFrame, point, x, y, sz)
+  end
+end
+
+function ReBar.prototype:FlipRowsColumns()
+  self.config.rows, self.config.columns = self.config.columns, self.config.rows
+  self.config.columnMajor = not self.config.columnMajor
+  self:ApplySize()
+  self:LayoutButtons()
+end
+
+function ReBar.prototype:AcquireButtons()
+  local n = self.config.rows * self.config.columns
   
   for i = 1, n do
     if self.buttons[i] == nil then
-      table.insert(self.buttons, self.class.button:acquire(self.barFrame, self.config.btnConfig, i))
+      local b = self.buttonClass:Acquire(self.config.btnConfig, i, self.config.pages and self.config.pages.n, n)
+      if b then
+        if not AceOO.inherits(b, ReBar.IButton) then
+          error("ReBar: specified Button class object did not meet required interface")
+        end
+        if self.locked == false then
+          b:BarUnlocked() -- buttons assume they are created in a barlocked state
+        end
+        self.buttons[i] = b
+        self.barFrame:SetAttribute("addchild",b:GetActionFrame())
+      end
     end
-    local b = self.buttons[i]
-    if b == nil then
-      break -- handling for button types that support limited numbers
-    end
-    b:PlaceButton("TOPLEFT", sp + gSize * math.fmod(i-1,c), - (sp + gSize * math.floor((i-1)/c)), sz)
   end
 
-  -- b == nil, above, should always be the case if and only if i == n. ReBar never monkeys
-  -- with buttons in the middle of the sequence: it always adds or removes on the array end
-  while #self.buttons > n do
-    self.class.button:release(table.remove(self.buttons))
+  local maxn = table.maxn(self.buttons)
+  for i = n+1, table.maxn(self.buttons) do
+    local b = self.buttons[i]
+    self.buttons[i] = nil
+    if b then
+      self.buttonClass:Release(b)
+    end
   end
   
 end
@@ -243,7 +639,7 @@
   -- no point if we can't store the name or the offsets are incomplete
   if name and x and y then
     self.config.anchor = { 
-      to = name,
+      frame = name,
       point = p,
       relPoint = rp or p,
       x = x,
@@ -254,45 +650,53 @@
 
 
 
+function ReBar.prototype:StickyIndicatorUpdate()
+  if IsShiftKeyDown() then
+    local snapRange = self.config.size + self.config.spacing
+    self:DisplaySnapIndicator(ReBar.anchorTargets, snapRange, 0, 0)
+  else
+    self:HideSnapIndicator()
+  end
+end
+
+
 -- mouse event handlers (clicking/dragging/resizing the bar)
 function ReBar.prototype:BeginDrag()
   local f = self.barFrame
   f:StartMoving()
   f.isMoving = true
-  f:SetScript("OnUpdate", function() self:StickyIndicatorUpdate() end)
+  self.updateTime = DRAG_UPDATE_RATE
+  f:SetScript("OnUpdate", function(frame, elapsed) self:StickyIndicatorUpdate(elapsed) end)
 end
 
 function ReBar.prototype:FinishDrag()
-  local f, p, rp, x, y 
+  local o, p, rp, x, y 
   local bf = self.barFrame
+  local snapRange = self.config.size + self.config.spacing
 
   bf:StopMovingOrSizing()
   bf.isMoving = false
 
   bf:SetScript("OnUpdate",nil)
   if IsShiftKeyDown() then
-    f, p, rp, x, y = self:GetStickyAnchor()
-    ReBarStickyIndicator1:Hide()
-    ReBarStickyIndicator2:Hide()
+    o, p, rp, x, y = self:GetClosestPointSnapped(ReBar.anchorTargets, snapRange, 0, 0)
   end
   
-  if f == nil then
-    f = UIParent
-    local _
-    _, p,rp,x,y = self:GetClosestPointTo(f)
+  if o == nil then
+    o, p, rp, x, y = self:GetClosestVisiblePoint(ReBar.UIParentAnchorTarget, snapRange, 0, 0)
   end
-
-  if f then
-    self:StoreAnchor(f,p,rp,x,y)
-    self:ApplyAnchor()
-  end
+  
+  self:HideSnapIndicator()
+  self:StoreAnchor(o:GetFrame(), p, rp, x, y)
+  self:ApplyAnchor()
 end
 
 function ReBar.prototype:BeginBarResize( sizingPoint )
   local f = self.barFrame
   f:StartSizing(sizingPoint)
   f.resizing = true
-  f:SetScript("OnUpdate",function() self:ReflowButtons() end)
+  self.updateTime = DRAG_UPDATE_RATE
+  f:SetScript("OnUpdate",function(frame, elapsed) self:ReflowButtons(elapsed) end)
 end
 
 function ReBar.prototype:BeginButtonResize( sizingPoint, mouseBtn )
@@ -303,12 +707,13 @@
   local c = self.config.columns
   local s = self.config.spacing
   local sz = self.config.size
+  self.updateTime = DRAG_UPDATE_RATE
   if mouseBtn == "LeftButton" then
-    f:SetMinResize(c*(12 + 2*s) +1, r*(12 + 2*s) +1)
-    f:SetScript("OnUpdate",function() self:DragSizeButtons() end)
+    f:SetMinResize(c*(12 + s) +0.1, r*(12 + s) +0.1)
+    f:SetScript("OnUpdate",function(frame, elapsed) self:DragSizeButtons(elapsed) end)
   elseif mouseBtn == "RightButton" then
-    f:SetMinResize(c*sz+1, r*sz+1)
-    f:SetScript("OnUpdate",function() self:DragSizeSpacing() end)
+    f:SetMinResize(c*sz+0.1, r*sz+0.1)
+    f:SetScript("OnUpdate",function(frame, elapsed) self:DragSizeSpacing(elapsed) end)
   end
 end
 
@@ -323,122 +728,6 @@
 
 
 
--- sticky anchoring functions
-function ReBar.prototype:StickyIndicatorUpdate()
-  local si1 = ReBarStickyIndicator1
-  local si2 = ReBarStickyIndicator2
-  if IsShiftKeyDown() then
-    local f, p, rp, x, y = self:GetStickyAnchor()
-    if f then
-      si1:ClearAllPoints()
-      si2:ClearAllPoints()
-      si1:SetPoint("CENTER",self.barFrame,p,0,0)
-      si2:SetPoint("CENTER",f,rp,x,y)
-      si1:Show()
-      si2:Show()
-      return nil
-    end
-  end
-  si1:Hide()
-  si2:Hide()
-  si1:ClearAllPoints()
-  si2:ClearAllPoints()
-end
-
-function ReBar.prototype:CheckAnchorable(f)
-  -- can't anchor to self or to a hidden frame
-  if f == self.barFrame or not(f:IsShown()) then return false end
-
-  -- also can't anchor to frames that are anchored to self
-  for i = 1, f:GetNumPoints() do
-    local _, f2 = f:GetPoint(i)
-    if f2 == self.barFrame then return false end
-  end
-
-  return true
-end
-
-
-function ReBar.prototype:GetStickyAnchor()
-  local snapRange = (self.config.size + self.config.spacing)
-  local r2, f, p, rp, x, y = self:GetClosestAnchor()
-
-  if f and p then
-    local xx, yy = pointFindTable[p](f) 
-    if r2 and r2 < (snapRange*snapRange) then
-      if xx or math.abs(x) < snapRange then x = 0 end
-      if yy or math.abs(y) < snapRange then y = 0 end
-    elseif not(yy) and math.abs(x) < snapRange then
-      x = 0
-    elseif not(xx) and math.abs(y) < snapRange then
-      y = 0
-    else
-      f = nil -- nothing in range
-    end
-  end
-  return f, p, rp, x, y
-end
-
-function ReBar.prototype:GetClosestAnchor()
-  -- choose the closest anchor point on the list of target frames
-  local range2, frame, point, relPoint, offsetX, offsetY
-
-  for f, tgtRegion in pairs(stickyTargets) do
-    if self:CheckAnchorable(f) then
-      local r2 ,p, rp, x, y = self:GetClosestPointTo(f,tgtRegion)
-      if r2 then
-        if not(range2 and range2 < r2) then
-          range2, frame, point, relPoint, offsetX, offsetY = r2, f, p, rp, x, y
-        end
-      end
-    end
-  end
-
-  return range2, frame, point, relPoint, offsetX, offsetY
-end
-
-function ReBar.prototype:GetClosestPointTo(f,inside)
-  local range2, point, relPoint, offsetX, offsetY
-  local pft = pointFindTable
-  local cx, cy = self.barFrame:GetCenter()
-  local fcx, fcy = f:GetCenter()
-  local fh = f:GetHeight()
-  local fw = f:GetWidth()
-
-  -- compute whether edge bisector intersects target edge
-  local dcx = math.abs(cx-fcx) < fw/2 and (cx-fcx)
-  local dcy = math.abs(cy-fcy) < fh/2 and (cy-fcy)
-  
-  for p, func in pairs(pft) do
-    local rp, x, y
-    if inside == outsideFrame then
-      rp = oppositePointTable[p]
-      x, y = self:GetOffsetToPoint(f, func, pft[rp])
-    else
-      rp = p
-      x, y = self:GetOffsetToPoint(f, func, func)
-    end
-
-    -- if anchoring to an edge, only anchor if the center point overlaps the other edge
-    if (x or dcx) and (y or dcy) then
-      local r2 = (x or 0)^2 + (y or 0)^2
-      if range2 == nil or r2 < range2 then
-        range2, point, relPoint, offsetX, offsetY = r2, p, rp, x or dcx, y or dcy
-      end
-    end
-  end
-  return range2, point, relPoint, offsetX, offsetY
-end
-
-function ReBar.prototype:GetOffsetToPoint(f,func,ffunc)
-  local x, y = func(self.barFrame)  -- coordinates of the point on this frame
-  local fx, fy = ffunc(f)  -- coordinates of the point on the target frame
-  -- guarantees: if x then fx, if y then fy
-  return x and (x-fx), y and (y-fy)
-end
-
-
-
 
 
 
@@ -449,47 +738,64 @@
   return f:GetWidth(), f:GetHeight(), c.size, c.rows, c.columns, c.spacing
 end
 
+
 -- add and remove buttons dynamically as the bar is resized
-function ReBar.prototype:ReflowButtons()
-  local w, h, sz, r, c, sp = self:GetLayout()
+function ReBar.prototype:ReflowButtons( elapsed )
+  self.updateTime = self.updateTime - elapsed
+  if self.updateTime <= 0 then
+    self.updateTime = DRAG_UPDATE_RATE
+  
+    local w, h, sz, r, c, sp = self:GetLayout()
 
-  self.config.rows = math.floor( (h - sp) / (sz + sp) )
-  self.config.columns = math.floor( (w - sp) / (sz + sp) )
+    self.config.rows = math.floor( (h+1) / (sz + sp) )
+    self.config.columns = math.floor( (w+1) / (sz + sp) )
 
-  if self.config.rows ~= r or self.config.columns ~= c then
-    self:LayoutButtons()
+    if self.config.rows ~= r or self.config.columns ~= c then
+      self:AcquireButtons()
+      self:LayoutButtons()
+    end
   end
 end
 
 
 -- change the size of buttons as the bar is resized
-function ReBar.prototype:DragSizeButtons()
-  local w, h, sz, r, c, sp = self:GetLayout()
+function ReBar.prototype:DragSizeButtons( elapsed )
+  self.updateTime = self.updateTime - elapsed
+  if self.updateTime <= 0 then
+    self.updateTime = DRAG_UPDATE_RATE
+  
+    local w, h, sz, r, c, sp = self:GetLayout()
 
-  local newSzW = math.floor((w - (c+1)*sp)/c)
-  local newSzH = math.floor((h - (r+1)*sp)/r)
-  
-  self.config.size = math.max(12, math.min(newSzW, newSzH))
+    local newSzW = math.floor((w - c*sp)/c)
+    local newSzH = math.floor((h - r*sp)/r)
+    
+    self.config.size = math.max(12, math.min(newSzW, newSzH))
 
-  if self.config.size ~= sz then
-    self:LayoutButtons()
-    self:UpdateResizeTooltip()
+    if self.config.size ~= sz then
+      self:LayoutButtons()
+      self:UpdateResizeTooltip()
+    end
   end
 end
 
 
 -- change the spacing of buttons as the bar is resized
-function ReBar.prototype:DragSizeSpacing()
-  local w, h, sz, r, c, sp = self:GetLayout()
+function ReBar.prototype:DragSizeSpacing( elapsed )
+  self.updateTime = self.updateTime - elapsed
+  if self.updateTime <= 0 then
+    self.updateTime = DRAG_UPDATE_RATE
+  
+    local w, h, sz, r, c, sp = self:GetLayout()
 
-  local newSpW = math.floor((w - c*sz)/(c+1))
-  local newSpH = math.floor((h - r*sz)/(r+1))
+    local newSpW = math.floor((w - c*sz)/c)
+    local newSpH = math.floor((h - r*sz)/r)
 
-  self.config.spacing = math.max(0, math.min(newSpW, newSpH))
+    self.config.spacing = math.max(0, math.min(newSpW, newSpH))
 
-  if self.config.spacing ~= sp then
-    self:LayoutButtons()
-    self:UpdateResizeTooltip()
+    if self.config.spacing ~= sp then
+      self:LayoutButtons()
+      self:UpdateResizeTooltip()
+    end
   end
 end
 
@@ -503,7 +809,7 @@
 
 function ReBar.prototype:ShowTooltip()
   GameTooltip:SetOwner(self.barFrame, "ANCHOR_TOPRIGHT")
-  GameTooltip:AddLine("Bar "..self.barID)
+  GameTooltip:AddLine(self.config.btnConfig.subtype.." Bar "..self.barID..(self.config.visible and "" or " (Hidden)"))
   GameTooltip:AddLine("Drag to move")
   GameTooltip:AddLine("Shift-drag for sticky mode")
   GameTooltip:AddLine("Right-click for options")