view classes/ReBar.lua @ 12:2735edcf9ab7

Version 0.34
author Flick <flickerstreak@gmail.com>
date Wed, 21 Mar 2007 00:13:27 +0000
parents f3a7bfebc283
children a805e4464237
line wrap: on
line source
-- private constants
local anchoredLabelColor    = { r = 1.0, g = 0.82, b = 0.0 }
local nonAnchoredLabelColor = { r = 1.0, g = 1.0,  b = 1.0 }

local DRAG_UPDATE_RATE = 0.125  -- cap at 8 Hz

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 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 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 = AceOO.Class("AceEvent-2.0", ReAnchor, ReAnchor.IAnchorable)


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

  self.config  = config
  self.barID   = id
  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
  local name = "ReBar"..id
  self.barFrame     = CreateFrame("Frame", name, config.parent and getglobal(config.parent) or UIParent, "ReBarTemplate")
  self.controlFrame = getglobal(self.barFrame:GetName().."Controls")
  self.controlFrame.reBar = self
  self.barFrame:SetClampedToScreen(true)

  -- get references to sub-frames
  self.labelString = getglobal(name.."ControlsLabelString")
  self.upArrow     = getglobal(name.."PageUp")
  self.downArrow   = getglobal(name.."PageDown")
  self.pageNum     = getglobal(name.."PageNumber")

  -- set the text label on the control widget
  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()

  if self.config.pages then
    self:SetPages(self.config.pages.n)
    self:ApplyAutoStanceSwitch()
    self:ApplyAutoStealthSwitch()
    self:RefreshPageControls()
  end

  -- add bar to anchorTargets list
  table.insert(ReBar.anchorTargets, self)
end

function ReBar.prototype:Destroy()
  local f = 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

  -- remove from anchorTargets table
  for idx, b in ipairs(ReBar.anchorTargets) do
    if b == self then
      table.remove(ReBar.anchorTargets, idx)
      break
    end
  end
  
  -- 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




-- 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
  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
function ReBar.prototype:GetVisibility()
  return self.config.visible
end

function ReBar.prototype:ToggleVisibility()
  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: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()
  if switch then
    -- check that the number of pages available is sufficient
    local totalPages = nStancePages[playerClass] + (self:GetAutoStealthSwitch() and 1 or 0)
    if self:GetPages() < totalPages then
      self:SetPages(totalPages)
    end
    for form, spec in pairs(stanceMaps[playerClass]) 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[playerClass]) 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()
  if switch then
    -- check that the number of pages available is sufficient
    local totalPages = (self:GetAutoStanceSwitch() and nStancePages[playerClass] > 0 and nStancePages[playerClass] or 1) + 1
    if self:GetPages() < totalPages then
      self:SetPages(totalPages)
    end
    local s, s2
    if playerClass == "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[playerClass])
    self.barFrame:SetAttribute("statemap-stealth-0",s2 or unstealthMaps[playerClass])
    -- 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   = self.upArrow
  local downArrow = self.downArrow
  local pageNum   = self.pageNum

  if self:GetPages() > 1 and self.config.pages.showControls then
    local loc = self.config.pages.controlsLoc

    upArrow:SetAttribute("hidestates",nil)
    downArrow:SetAttribute("hidestates",nil)

    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:SetAttribute("hidestates","1-"..self:GetPages())
    downArrow:SetAttribute("hidestates","1-"..self:GetPages())

    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()
  local buttonSz = self.config.size or 36
  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
  local h = buttonSz * rows + spacing * rows
  local f = self.barFrame

  -- + 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()
  local a = self.config.anchor
  local f = self.barFrame
  if a then
    f:ClearAllPoints()
    f:SetPoint(a.point,getglobal(a.frame),a.relPoint,a.x,a.y)
    local color = anchoredLabelColor
    if a.frame == "UIParent" or a.frame == "WorldFrame" then
      color = nonAnchoredLabelColor
    end
    self.labelString:SetTextColor(color.r, color.g, color.b)
  end
end

function ReBar.prototype:ApplyVisibility()
  local v = self.config.visible or not self.locked
  
  if v then
    self.barFrame:Show()
  else
    self.barFrame:Hide()
  end
end

function ReBar.prototype:LayoutButtons()
  local r = self.config.rows
  local c = self.config.columns
  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
      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
  end

  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


function ReBar.prototype:StoreAnchor(f, p, rp, x, y)
  local name = f:GetName()
  -- no point if we can't store the name or the offsets are incomplete
  if name and x and y then
    self.config.anchor = { 
      frame = name,
      point = p,
      relPoint = rp or p,
      x = x,
      y = y
    }
  end
end  



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
  self.updateTime = DRAG_UPDATE_RATE
  f:SetScript("OnUpdate", function(frame, elapsed) self:StickyIndicatorUpdate(elapsed) end)
end

function ReBar.prototype:FinishDrag()
  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
    o, p, rp, x, y = self:GetClosestPointSnapped(ReBar.anchorTargets, snapRange, 0, 0)
  end
  
  if o == nil then
    o, p, rp, x, y = self:GetClosestVisiblePoint(ReBar.UIParentAnchorTarget, snapRange, 0, 0)
  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
  self.updateTime = DRAG_UPDATE_RATE
  f:SetScript("OnUpdate",function(frame, elapsed) self:ReflowButtons(elapsed) end)
end

function ReBar.prototype:BeginButtonResize( sizingPoint, mouseBtn )
  local f = self.barFrame
  f:StartSizing(sizingPoint)
  f.resizing = true
  local r = self.config.rows
  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 + 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+0.1, r*sz+0.1)
    f:SetScript("OnUpdate",function(frame, elapsed) self:DragSizeSpacing(elapsed) end)
  end
end

function ReBar.prototype:FinishResize()
  local f = self.barFrame
  f:StopMovingOrSizing()
  f.resizing = false
  f:SetScript("OnUpdate",nil)
  self:ApplySize()
end







-- utility function to get the height, width, and button size attributes
function ReBar.prototype:GetLayout()
  local c = self.config
  local f = self.barFrame
  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( 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+1) / (sz + sp) )
    self.config.columns = math.floor( (w+1) / (sz + sp) )

    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( 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*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()
    end
  end
end


-- change the spacing of buttons as the bar is resized
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)
    local newSpH = math.floor((h - r*sz)/r)

    self.config.spacing = math.max(0, math.min(newSpW, newSpH))

    if self.config.spacing ~= sp then
      self:LayoutButtons()
      self:UpdateResizeTooltip()
    end
  end
end


-- update the drag tooltip to indicate current sizes
function ReBar.prototype:UpdateResizeTooltip()
  GameTooltipTextRight4:SetText(self.config.size)
  GameTooltipTextRight5:SetText(self.config.spacing)
  GameTooltip:Show()
end

function ReBar.prototype:ShowTooltip()
  GameTooltip:SetOwner(self.barFrame, "ANCHOR_TOPRIGHT")
  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")
  GameTooltip:Show()
end

function ReBar.prototype:ShowButtonResizeTooltip(point)
  GameTooltip:SetOwner(self.barFrame, "ANCHOR_"..point)
  GameTooltip:AddLine("Drag to resize buttons")
  GameTooltip:AddLine("Right-click-drag")
  GameTooltip:AddLine("to change spacing")
  GameTooltip:AddDoubleLine("Size: ", "0")
  GameTooltip:AddDoubleLine("Spacing: ", "0")
  self:UpdateResizeTooltip()
end

function ReBar.prototype:ShowBarResizeTooltip(point)
  GameTooltip:SetOwner(self.barFrame, "ANCHOR_"..point)
  GameTooltip:AddLine("Drag to add/remove buttons")
  GameTooltip:Show()
end